diff --git a/engine/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt b/engine/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt index f8544be53f..2dd7651e25 100644 --- a/engine/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt +++ b/engine/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt @@ -19,10 +19,12 @@ package com.google.android.fhir.db.impl import android.content.Context import androidx.test.core.app.ApplicationProvider import androidx.test.filters.MediumTest +import ca.uhn.fhir.rest.gclient.StringClientParam import ca.uhn.fhir.rest.param.ParamPrefixEnum import com.google.android.fhir.DateProvider import com.google.android.fhir.FhirServices import com.google.android.fhir.LocalChange +import com.google.android.fhir.db.Database import com.google.android.fhir.db.ResourceNotFoundException import com.google.android.fhir.db.impl.dao.toLocalChange import com.google.android.fhir.db.impl.entities.LocalChangeEntity @@ -50,7 +52,9 @@ import org.hl7.fhir.r4.model.DateTimeType import org.hl7.fhir.r4.model.DateType import org.hl7.fhir.r4.model.DecimalType import org.hl7.fhir.r4.model.Enumerations +import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.HumanName +import org.hl7.fhir.r4.model.Identifier import org.hl7.fhir.r4.model.Immunization import org.hl7.fhir.r4.model.Meta import org.hl7.fhir.r4.model.Observation @@ -60,6 +64,8 @@ import org.hl7.fhir.r4.model.Quantity import org.hl7.fhir.r4.model.Reference import org.hl7.fhir.r4.model.ResourceType import org.hl7.fhir.r4.model.RiskAssessment +import org.hl7.fhir.r4.model.SearchParameter +import org.hl7.fhir.r4.model.StringType import org.json.JSONArray import org.junit.After import org.junit.Assert.assertThrows @@ -84,15 +90,28 @@ class DatabaseImplTest { @JvmField @Parameterized.Parameter(0) var encrypted: Boolean = false private val context: Context = ApplicationProvider.getApplicationContext() - private val services = - FhirServices.builder(context) - .inMemory() - .apply { if (encrypted) enableEncryptionIfSupported() } - .build() - private val testingUtils = TestingUtils(services.parser) - private val database = services.database - - @Before fun setUp(): Unit = runBlocking { database.insert(TEST_PATIENT_1) } + private lateinit var services: FhirServices + private lateinit var testingUtils: TestingUtils + private lateinit var database: Database + + @Before + fun setUp(): Unit = runBlocking { + buildFhirService() + database.insert(TEST_PATIENT_1) + } + + private fun buildFhirService(customSearchParameter: List? = null) { + services = + FhirServices.builder(context) + .inMemory() + .apply { + if (encrypted) enableEncryptionIfSupported() + setSearchParameters(customSearchParameter) + } + .build() + database = services.database + testingUtils = TestingUtils(services.parser) + } @After fun tearDown() { @@ -451,10 +470,13 @@ class DatabaseImplTest { fun delete_nonExistent_shouldNotInsertLocalChange() = runBlocking { database.delete(ResourceType.Patient, "nonexistent_patient") assertThat( - database.getAllLocalChanges().map { it }.none { - it.localChange.type.equals(LocalChangeEntity.Type.DELETE) && - it.localChange.resourceId.equals("nonexistent_patient") - } + database + .getAllLocalChanges() + .map { it } + .none { + it.localChange.type.equals(LocalChangeEntity.Type.DELETE) && + it.localChange.resourceId.equals("nonexistent_patient") + } ) .isTrue() } @@ -480,9 +502,10 @@ class DatabaseImplTest { val patient: Patient = testingUtils.readFromFile(Patient::class.java, "/date_test_patient.json") database.insertRemote(patient) assertThat( - database.getAllLocalChanges().map { it }.none { - it.localChange.resourceId.equals(patient.logicalId) - } + database + .getAllLocalChanges() + .map { it } + .none { it.localChange.resourceId.equals(patient.logicalId) } ) .isTrue() } @@ -532,15 +555,17 @@ class DatabaseImplTest { } database.insert(patient) services.fhirEngine.syncUpload { it -> - it.first { it.resourceId == "remote-patient-3" }.let { - flowOf( - it.token to - Patient().apply { - id = it.resourceId - meta = remoteMeta - } - ) - } + it + .first { it.resourceId == "remote-patient-3" } + .let { + flowOf( + it.token to + Patient().apply { + id = it.resourceId + meta = remoteMeta + } + ) + } } val selectedEntity = database.selectEntity(ResourceType.Patient, "remote-patient-3") assertThat(selectedEntity.versionId).isEqualTo(remoteMeta.versionId) @@ -552,9 +577,10 @@ class DatabaseImplTest { val patient: Patient = testingUtils.readFromFile(Patient::class.java, "/date_test_patient.json") database.insertRemote(patient, TEST_PATIENT_2) assertThat( - database.getAllLocalChanges().map { it }.none { - it.localChange.resourceId in listOf(patient.logicalId, TEST_PATIENT_2_ID) - } + database + .getAllLocalChanges() + .map { it } + .none { it.localChange.resourceId in listOf(patient.logicalId, TEST_PATIENT_2_ID) } ) .isTrue() } @@ -2547,7 +2573,8 @@ class DatabaseImplTest { ) assertThat( - database.search( + database + .search( Search(ResourceType.Patient) .apply { sort(Patient.BIRTHDATE, Order.DESCENDING) } .getQuery() @@ -2574,7 +2601,8 @@ class DatabaseImplTest { ) assertThat( - database.search( + database + .search( Search(ResourceType.Patient) .apply { sort(Patient.BIRTHDATE, Order.ASCENDING) } .getQuery() @@ -2832,6 +2860,129 @@ class DatabaseImplTest { .inOrder() } + @Test + fun search_patient_with_extension_as_search_param() = runBlocking { + val maidenNameSearchParameter = + SearchParameter().apply { + url = "http://example.com/SearchParameter/patient-mothersMaidenName" + addBase("Patient") + name = "mothers-maiden-name" + code = "mothers-maiden-name" + type = Enumerations.SearchParamType.STRING + expression = + "Patient.extension('http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName').value.as(String)" + description = "search on mother's maiden name" + } + val patient = + Patient().apply { + addIdentifier( + Identifier().apply { + system = "https://custom-identifier-namespace" + value = "OfficialIdentifier_DarcySmith_0001" + } + ) + + addName( + HumanName().apply { + use = HumanName.NameUse.OFFICIAL + family = "Smith" + addGiven("Darcy") + gender = Enumerations.AdministrativeGender.FEMALE + birthDateElement = DateType("1970-01-01") + } + ) + + addExtension( + Extension().apply { + url = "http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName" + setValue(StringType("Marca")) + } + ) + } + // Get rid of the default service and create one with search params + tearDown() + buildFhirService(listOf(maidenNameSearchParameter)) + database.insert(patient) + + val result = + database.search( + Search(ResourceType.Patient) + .apply { + filter( + StringClientParam("mothers-maiden-name"), + { + value = "Marca" + modifier = StringFilterModifier.MATCHES_EXACTLY + } + ) + } + .getQuery() + ) + + assertThat(result.map { it.nameFirstRep.nameAsSingleString }).contains("Darcy Smith") + } + + @Test + fun search_patient_with_custom_value_as_search_param() = runBlocking { + val patient = + Patient().apply { + addIdentifier( + Identifier().apply { + system = "https://custom-identifier-namespace" + value = "OfficialIdentifier_DarcySmith_0001" + } + ) + + addName( + HumanName().apply { + use = HumanName.NameUse.OFFICIAL + family = "Smith" + addGiven("Darcy") + gender = Enumerations.AdministrativeGender.FEMALE + birthDateElement = DateType("1970-01-01") + } + ) + + addExtension( + Extension().apply { + url = "http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName" + setValue(StringType("Marca")) + } + ) + } + val identifierPartialSearchParameter = + SearchParameter().apply { + url = "http://example.com/SearchParameter/patient-identifierPartial" + addBase("Patient") + name = "identifierPartial" + code = "identifierPartial" + type = Enumerations.SearchParamType.STRING + expression = "Patient.identifier.value" + description = "Search the identifier" + } + // Get rid of the default service and create one with search params + tearDown() + buildFhirService(listOf(identifierPartialSearchParameter)) + database.insert(patient) + + val result = + database.search( + Search(ResourceType.Patient) + .apply { + filter( + StringClientParam("identifierPartial"), + { + value = "OfficialIdentifier_" + modifier = StringFilterModifier.STARTS_WITH + } + ) + } + .getQuery() + ) + + assertThat(result.map { it.nameFirstRep.nameAsSingleString }).contains("Darcy Smith") + } + private companion object { const val mockEpochTimeStamp = 1628516301000 const val TEST_PATIENT_1_ID = "test_patient_1" diff --git a/engine/src/androidTest/java/com/google/android/fhir/db/impl/EncryptedDatabaseErrorTest.kt b/engine/src/androidTest/java/com/google/android/fhir/db/impl/EncryptedDatabaseErrorTest.kt index 1cfeeea9dd..eb52f8db26 100644 --- a/engine/src/androidTest/java/com/google/android/fhir/db/impl/EncryptedDatabaseErrorTest.kt +++ b/engine/src/androidTest/java/com/google/android/fhir/db/impl/EncryptedDatabaseErrorTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,8 @@ import com.google.android.fhir.DatabaseErrorStrategy.UNSPECIFIED import com.google.android.fhir.db.impl.DatabaseImpl.Companion.DATABASE_PASSPHRASE_NAME import com.google.android.fhir.db.impl.DatabaseImpl.Companion.ENCRYPTED_DATABASE_NAME import com.google.android.fhir.db.impl.DatabaseImpl.Companion.UNENCRYPTED_DATABASE_NAME +import com.google.android.fhir.index.ResourceIndexer +import com.google.android.fhir.index.SearchParamDefinitionsProviderImpl import com.google.android.fhir.search.Order import com.google.android.fhir.search.Search import com.google.android.fhir.search.getQuery @@ -46,6 +48,7 @@ import org.junit.runner.RunWith class EncryptedDatabaseErrorTest { private val context: Context = ApplicationProvider.getApplicationContext() private val parser = FhirContext.forR4().newJsonParser() + private val resourceIndexer = ResourceIndexer(SearchParamDefinitionsProviderImpl()) @After fun tearDown() { @@ -65,7 +68,8 @@ class EncryptedDatabaseErrorTest { inMemory = false, enableEncryption = false, databaseErrorStrategy = UNSPECIFIED - ) + ), + resourceIndexer ) .let { it.insert(TEST_PATIENT_1) @@ -81,7 +85,8 @@ class EncryptedDatabaseErrorTest { inMemory = false, enableEncryption = true, databaseErrorStrategy = UNSPECIFIED - ) + ), + resourceIndexer ) .let { it.search( @@ -110,7 +115,8 @@ class EncryptedDatabaseErrorTest { inMemory = false, enableEncryption = true, databaseErrorStrategy = UNSPECIFIED - ) + ), + resourceIndexer ) .let { it.insert(TEST_PATIENT_1) @@ -132,7 +138,8 @@ class EncryptedDatabaseErrorTest { inMemory = false, enableEncryption = true, databaseErrorStrategy = UNSPECIFIED - ) + ), + resourceIndexer ) .let { it.search( @@ -160,7 +167,8 @@ class EncryptedDatabaseErrorTest { inMemory = false, enableEncryption = true, databaseErrorStrategy = UNSPECIFIED - ) + ), + resourceIndexer ) .let { it.insert(TEST_PATIENT_1) @@ -182,7 +190,8 @@ class EncryptedDatabaseErrorTest { inMemory = false, enableEncryption = true, databaseErrorStrategy = RECREATE_AT_OPEN - ) + ), + resourceIndexer ) .let { assertThat( @@ -213,7 +222,8 @@ class EncryptedDatabaseErrorTest { inMemory = false, enableEncryption = true, databaseErrorStrategy = UNSPECIFIED - ) + ), + resourceIndexer ) .let { it.insert(TEST_PATIENT_1) @@ -229,7 +239,8 @@ class EncryptedDatabaseErrorTest { inMemory = false, enableEncryption = false, databaseErrorStrategy = UNSPECIFIED - ) + ), + resourceIndexer ) .let { assertThat( diff --git a/engine/src/main/java/com/google/android/fhir/FhirEngineProvider.kt b/engine/src/main/java/com/google/android/fhir/FhirEngineProvider.kt index 2269d31266..5e657cbf80 100644 --- a/engine/src/main/java/com/google/android/fhir/FhirEngineProvider.kt +++ b/engine/src/main/java/com/google/android/fhir/FhirEngineProvider.kt @@ -21,6 +21,7 @@ import com.google.android.fhir.DatabaseErrorStrategy.UNSPECIFIED import com.google.android.fhir.sync.Authenticator import com.google.android.fhir.sync.DataSource import com.google.android.fhir.sync.remote.HttpLogger +import org.hl7.fhir.r4.model.SearchParameter /** The provider for [FhirEngine] instance. */ object FhirEngineProvider { @@ -68,6 +69,7 @@ object FhirEngineProvider { if (configuration.enableEncryptionIfSupported) enableEncryptionIfSupported() setDatabaseErrorStrategy(configuration.databaseErrorStrategy) configuration.serverConfiguration?.let { setServerConfiguration(it) } + configuration.customSearchParameters?.let { setSearchParameters(it) } if (configuration.testMode) { inMemory() } @@ -105,7 +107,20 @@ data class FhirEngineConfiguration( val enableEncryptionIfSupported: Boolean = false, val databaseErrorStrategy: DatabaseErrorStrategy = UNSPECIFIED, val serverConfiguration: ServerConfiguration? = null, - val testMode: Boolean = false + val testMode: Boolean = false, + /** + * Additional search parameters to be used to query FHIR engine using the search API. These are in + * addition to the default search parameters defined in + * [FHIR](https://www.hl7.org/fhir/searchparameter-registry.html). The search parameters should be + * unique and not change the existing/default search parameters and it may lead to unexpected + * search behaviour. + * + * NOTE: The engine doesn't reindex resources after a new [SearchParameter] is added to the + * engine. It is the responsibility of the app developer to reindex the resources by updating + * them. Any new CRUD operations on a resource after a new [SearchParameter] is added will result + * in the reindexing of the resource. + */ + val customSearchParameters: List? = null ) enum class DatabaseErrorStrategy { diff --git a/engine/src/main/java/com/google/android/fhir/FhirServices.kt b/engine/src/main/java/com/google/android/fhir/FhirServices.kt index c81afdc6d5..eb11475d81 100644 --- a/engine/src/main/java/com/google/android/fhir/FhirServices.kt +++ b/engine/src/main/java/com/google/android/fhir/FhirServices.kt @@ -25,8 +25,11 @@ import com.google.android.fhir.db.impl.DatabaseConfig import com.google.android.fhir.db.impl.DatabaseEncryptionKeyProvider.isDatabaseEncryptionSupported import com.google.android.fhir.db.impl.DatabaseImpl import com.google.android.fhir.impl.FhirEngineImpl +import com.google.android.fhir.index.ResourceIndexer +import com.google.android.fhir.index.SearchParamDefinitionsProviderImpl import com.google.android.fhir.sync.DataSource import com.google.android.fhir.sync.remote.RemoteFhirService +import org.hl7.fhir.r4.model.SearchParameter import timber.log.Timber internal data class FhirServices( @@ -40,6 +43,7 @@ internal data class FhirServices( private var enableEncryption: Boolean = false private var databaseErrorStrategy = DatabaseErrorStrategy.UNSPECIFIED private var serverConfiguration: ServerConfiguration? = null + private var searchParameters: List? = null internal fun inMemory() = apply { inMemory = true } @@ -59,13 +63,20 @@ internal data class FhirServices( this.serverConfiguration = serverConfiguration } + internal fun setSearchParameters(searchParameters: List?) = apply { + this.searchParameters = searchParameters + } + fun build(): FhirServices { val parser = FhirContext.forCached(FhirVersionEnum.R4).newJsonParser() + val searchParamMap = + searchParameters?.asMapOfResourceTypeToSearchParamDefinitions() ?: emptyMap() val db = DatabaseImpl( context = context, iParser = parser, - DatabaseConfig(inMemory, enableEncryption, databaseErrorStrategy) + DatabaseConfig(inMemory, enableEncryption, databaseErrorStrategy), + resourceIndexer = ResourceIndexer(SearchParamDefinitionsProviderImpl(searchParamMap)) ) val engine = FhirEngineImpl(database = db, context = context) val remoteDataSource = @@ -79,7 +90,7 @@ internal data class FhirServices( fhirEngine = engine, parser = parser, database = db, - remoteDataSource = remoteDataSource + remoteDataSource = remoteDataSource, ) } } diff --git a/engine/src/main/java/com/google/android/fhir/MoreSearchParameters.kt b/engine/src/main/java/com/google/android/fhir/MoreSearchParameters.kt new file mode 100644 index 0000000000..f180538e48 --- /dev/null +++ b/engine/src/main/java/com/google/android/fhir/MoreSearchParameters.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir + +import com.google.android.fhir.index.SearchParamDefinition +import org.hl7.fhir.r4.model.SearchParameter + +/** + * Converts a list of [SearchParameter]s into a Map of resourceType string and list of associated + * [SearchParamDefinition]. + */ +internal fun List.asMapOfResourceTypeToSearchParamDefinitions(): + Map> = + flatMap { it.toSearchParamDefinition() }.groupBy({ it.first }, { it.second }) + +/** @return List of pairs of resourceType string and associated [SearchParamDefinition]. */ +internal fun SearchParameter.toSearchParamDefinition(): List> { + require(!name.isNullOrEmpty()) { "SearchParameter.name can't be null or empty." } + + requireNotNull(type) { "SearchParameter.type can't be null." } + + require(!expression.isNullOrEmpty()) { "SearchParameter.expression can't be null or empty." } + + return getResourceToPathMap(this).map { (resourceType, path) -> + resourceType to SearchParamDefinition(name = name, type = type, path = path) + } +} + +private fun getResourceToPathMap(searchParam: SearchParameter): Map { + // the if block is added because of the issue https://jira.hl7.org/browse/FHIR-22724 and can + // be removed once the issue is resolved + return if (searchParam.base.size == 1) { + mapOf(searchParam.base.single().valueAsString to searchParam.expression) + } else { + searchParam.expression + .split("|") + .groupBy { splitString -> splitString.split(".").first().trim().removePrefix("(") } + .mapValues { it.value.joinToString(" | ") { join -> join.trim() } } + } +} diff --git a/engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt b/engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt index 0be6124821..b075ff5104 100644 --- a/engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt +++ b/engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt @@ -31,9 +31,9 @@ import com.google.android.fhir.db.impl.dao.SquashedLocalChange import com.google.android.fhir.db.impl.entities.LocalChangeEntity import com.google.android.fhir.db.impl.entities.ResourceEntity import com.google.android.fhir.db.impl.entities.SyncedResourceEntity +import com.google.android.fhir.index.ResourceIndexer import com.google.android.fhir.logicalId import com.google.android.fhir.search.SearchQuery -import java.lang.IllegalStateException import java.time.Instant import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.ResourceType @@ -46,7 +46,8 @@ import org.hl7.fhir.r4.model.ResourceType internal class DatabaseImpl( private val context: Context, private val iParser: IParser, - databaseConfig: DatabaseConfig + databaseConfig: DatabaseConfig, + private val resourceIndexer: ResourceIndexer ) : com.google.android.fhir.db.Database { val db: ResourceDatabase @@ -96,7 +97,12 @@ internal class DatabaseImpl( .build() } - private val resourceDao by lazy { db.resourceDao().also { it.iParser = iParser } } + private val resourceDao by lazy { + db.resourceDao().also { + it.iParser = iParser + it.resourceIndexer = resourceIndexer + } + } private val syncedResourceDao = db.syncedResourceDao() private val localChangeDao = db.localChangeDao().also { it.iParser = iParser } @@ -145,8 +151,7 @@ internal class DatabaseImpl( iParser.parseResource(it) } ?: throw ResourceNotFoundException(type.name, id) - } as - Resource + } as Resource } override suspend fun lastUpdate(resourceType: ResourceType): String? { @@ -202,9 +207,12 @@ internal class DatabaseImpl( */ override suspend fun getAllLocalChanges(): List { return db.withTransaction { - localChangeDao.getAllLocalChanges().groupBy { it.resourceId to it.resourceType }.values.map { - SquashedLocalChange(LocalChangeToken(it.map { it.id }), LocalChangeUtils.squash(it)) - } + localChangeDao + .getAllLocalChanges() + .groupBy { it.resourceId to it.resourceType } + .values.map { + SquashedLocalChange(LocalChangeToken(it.map { it.id }), LocalChangeUtils.squash(it)) + } } } diff --git a/engine/src/main/java/com/google/android/fhir/db/impl/dao/ResourceDao.kt b/engine/src/main/java/com/google/android/fhir/db/impl/dao/ResourceDao.kt index 8633c6b1bc..96055acb05 100644 --- a/engine/src/main/java/com/google/android/fhir/db/impl/dao/ResourceDao.kt +++ b/engine/src/main/java/com/google/android/fhir/db/impl/dao/ResourceDao.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,6 +49,7 @@ internal abstract class ResourceDao { // this is ugly but there is no way to inject these right now in Room as it is the one creating // the dao lateinit var iParser: IParser + lateinit var resourceIndexer: ResourceIndexer open suspend fun update(resource: Resource) { updateResource( @@ -67,7 +68,7 @@ internal abstract class ResourceDao { versionId = it.versionId, lastUpdatedRemote = it.lastUpdatedRemote ) - val index = ResourceIndexer.index(resource) + val index = resourceIndexer.index(resource) updateIndicesForResource(index, entity, it.resourceUuid) } ?: throw ResourceNotFoundException(resource.resourceType.name, resource.id) @@ -191,7 +192,7 @@ internal abstract class ResourceDao { lastUpdatedRemote = resource.lastUpdated ) insertResource(entity) - val index = ResourceIndexer.index(resource) + val index = resourceIndexer.index(resource) updateIndicesForResource(index, entity, resourceUuid) return resource.id diff --git a/engine/src/main/java/com/google/android/fhir/index/ResourceIndexer.kt b/engine/src/main/java/com/google/android/fhir/index/ResourceIndexer.kt index 9ea8560caf..46b0277601 100644 --- a/engine/src/main/java/com/google/android/fhir/index/ResourceIndexer.kt +++ b/engine/src/main/java/com/google/android/fhir/index/ResourceIndexer.kt @@ -63,7 +63,9 @@ import org.hl7.fhir.r4.utils.FHIRPathEngine * Indexes a FHIR resource according to the * [search parameters](https://www.hl7.org/fhir/searchparameter-registry.html). */ -internal object ResourceIndexer { +internal class ResourceIndexer( + private val searchParamDefinitionsProvider: SearchParamDefinitionsProvider +) { // Switched HapiWorkerContext to SimpleWorkerContext as a fix for // https://github.com/google/android-fhir/issues/768 private val fhirPathEngine = FHIRPathEngine(SimpleWorkerContext()) @@ -72,7 +74,8 @@ internal object ResourceIndexer { private fun extractIndexValues(resource: R): ResourceIndices { val indexBuilder = ResourceIndices.Builder(resource.resourceType, resource.logicalId) - getSearchParamList(resource) + searchParamDefinitionsProvider + .get(resource) .map { it to fhirPathEngine.evaluate(resource, it.path) } .flatMap { pair -> pair.second.map { pair.first to it } } .forEach { pair -> @@ -400,11 +403,13 @@ internal object ResourceIndexer { } } - /** - * The FHIR currency code system. See: https://bit.ly/30YB3ML. See: - * https://www.hl7.org/fhir/valueset-currencies.html. - */ - private const val FHIR_CURRENCY_CODE_SYSTEM = "urn:iso:std:iso:4217" + companion object { + /** + * The FHIR currency code system. See: https://bit.ly/30YB3ML. See: + * https://www.hl7.org/fhir/valueset-currencies.html. + */ + private const val FHIR_CURRENCY_CODE_SYSTEM = "urn:iso:std:iso:4217" + } } data class SearchParamDefinition(val name: String, val type: SearchParamType, val path: String) diff --git a/engine/src/main/java/com/google/android/fhir/index/SearchParamDefinitionsProvider.kt b/engine/src/main/java/com/google/android/fhir/index/SearchParamDefinitionsProvider.kt new file mode 100644 index 0000000000..f8ed84ef64 --- /dev/null +++ b/engine/src/main/java/com/google/android/fhir/index/SearchParamDefinitionsProvider.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.index + +import org.hl7.fhir.r4.model.Resource + +/** Provides a list of [SearchParamDefinition]s for a [Resource]. */ +internal fun interface SearchParamDefinitionsProvider { + + /** @return [SearchParamDefinition]s based on the [Resource.fhirType]. */ + fun get(resource: Resource): List +} + +/** + * An implementation of [SearchParamDefinitionsProvider] that provides the [List]< + * [SearchParamDefinition]> from the default params and custom params(if any). + */ +internal class SearchParamDefinitionsProviderImpl( + private val customParams: Map> = emptyMap() +) : SearchParamDefinitionsProvider { + + override fun get(resource: Resource): List { + return getSearchParamList(resource) + + customParams.getOrDefault(resource.fhirType(), emptyList()) + } +} diff --git a/engine/src/test/java/com/google/android/fhir/MoreSearchParametersTest.kt b/engine/src/test/java/com/google/android/fhir/MoreSearchParametersTest.kt new file mode 100644 index 0000000000..bd6395fa8a --- /dev/null +++ b/engine/src/test/java/com/google/android/fhir/MoreSearchParametersTest.kt @@ -0,0 +1,225 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir + +import com.google.android.fhir.index.SearchParamDefinition +import com.google.common.truth.Truth.assertThat +import org.hl7.fhir.r4.model.Enumerations +import org.hl7.fhir.r4.model.SearchParameter +import org.junit.Assert.assertThrows +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class MoreSearchParametersTest { + @Test + fun asMapOfResourceTypeToSearchParamDefinitions() { + + val familyNameSearchParameter = + SearchParameter().apply { + name = "family" + expression = "Patient.name.family | Practitioner.name.family" + addBase("Patient") + addBase("Practitioner") + type = Enumerations.SearchParamType.STRING + } + + val genderSearchParameter = + SearchParameter().apply { + name = "gender" + expression = "Patient.gender | Person.gender | Practitioner.gender | RelatedPerson.gender" + addBase("Patient") + addBase("Person") + addBase("Practitioner") + addBase("RelatedPerson") + type = Enumerations.SearchParamType.TOKEN + } + + val result = + listOf(familyNameSearchParameter, genderSearchParameter) + .asMapOfResourceTypeToSearchParamDefinitions() + + assertThat(result) + .containsEntry( + "Patient", + listOf( + SearchParamDefinition( + name = "family", + type = Enumerations.SearchParamType.STRING, + path = "Patient.name.family" + ), + SearchParamDefinition( + name = "gender", + type = Enumerations.SearchParamType.TOKEN, + path = "Patient.gender" + ) + ) + ) + assertThat(result) + .containsEntry( + "Practitioner", + listOf( + SearchParamDefinition( + name = "family", + type = Enumerations.SearchParamType.STRING, + path = "Practitioner.name.family" + ), + SearchParamDefinition( + name = "gender", + type = Enumerations.SearchParamType.TOKEN, + path = "Practitioner.gender" + ) + ) + ) + assertThat(result) + .containsEntry( + "Person", + listOf( + SearchParamDefinition( + name = "gender", + type = Enumerations.SearchParamType.TOKEN, + path = "Person.gender" + ) + ) + ) + assertThat(result) + .containsEntry( + "RelatedPerson", + listOf( + SearchParamDefinition( + name = "gender", + type = Enumerations.SearchParamType.TOKEN, + path = "RelatedPerson.gender" + ) + ) + ) + } + + @Test + fun toSearchParamDefinition() { + val familySearchParameter = + SearchParameter().apply { + name = "family" + expression = "Patient.name.family | Practitioner.name.family" + addBase("Patient") + addBase("Practitioner") + type = Enumerations.SearchParamType.STRING + } + + val result = familySearchParameter.toSearchParamDefinition() + assertThat(result) + .containsExactly( + "Patient" to + SearchParamDefinition( + name = "family", + type = Enumerations.SearchParamType.STRING, + path = "Patient.name.family" + ), + "Practitioner" to + SearchParamDefinition( + name = "family", + type = Enumerations.SearchParamType.STRING, + path = "Practitioner.name.family" + ) + ) + } + + @Test + fun toSearchParamDefinition_throws_IllegalArgumentException_when_name_is_null_or_empty() { + val nullNameSearchParameter = + SearchParameter().apply { + name = null + expression = "Patient.name.family | Practitioner.name.family" + addBase("Patient") + addBase("Practitioner") + type = Enumerations.SearchParamType.STRING + } + + var exception = + assertThrows(null, IllegalArgumentException::class.java) { + nullNameSearchParameter.toSearchParamDefinition() + } + assertThat(exception.message).isEqualTo("SearchParameter.name can't be null or empty.") + + val emptyNameSearchParameter = + SearchParameter().apply { + name = "" + expression = "Patient.name.family | Practitioner.name.family" + addBase("Patient") + addBase("Practitioner") + type = Enumerations.SearchParamType.STRING + } + + exception = + assertThrows(null, IllegalArgumentException::class.java) { + emptyNameSearchParameter.toSearchParamDefinition() + } + assertThat(exception.message).isEqualTo("SearchParameter.name can't be null or empty.") + } + + @Test + fun toSearchParamDefinition_throws_IllegalArgumentException_when_expression_is_null_or_empty() { + val nullExpressionSearchParameter = + SearchParameter().apply { + name = "family" + expression = null + addBase("Patient") + addBase("Practitioner") + type = Enumerations.SearchParamType.STRING + } + + var exception = + assertThrows(null, IllegalArgumentException::class.java) { + nullExpressionSearchParameter.toSearchParamDefinition() + } + assertThat(exception.message).isEqualTo("SearchParameter.expression can't be null or empty.") + + val emptyExpressionSearchParameter = + SearchParameter().apply { + name = "family" + expression = "" + addBase("Patient") + addBase("Practitioner") + type = Enumerations.SearchParamType.STRING + } + + exception = + assertThrows(null, IllegalArgumentException::class.java) { + emptyExpressionSearchParameter.toSearchParamDefinition() + } + assertThat(exception.message).isEqualTo("SearchParameter.expression can't be null or empty.") + } + + @Test + fun toSearchParamDefinition_throws_IllegalArgumentException_when_type_is_null() { + val nullTypeSearchParameter = + SearchParameter().apply { + name = "family" + expression = "Patient.name.family | Practitioner.name.family" + addBase("Patient") + addBase("Practitioner") + type = null + } + + val exception = + assertThrows(null, IllegalArgumentException::class.java) { + nullTypeSearchParameter.toSearchParamDefinition() + } + assertThat(exception.message).isEqualTo("SearchParameter.type can't be null.") + } +} diff --git a/engine/src/test/java/com/google/android/fhir/index/ResourceIndexerTest.kt b/engine/src/test/java/com/google/android/fhir/index/ResourceIndexerTest.kt index c1b8fdfde9..1b41962b67 100644 --- a/engine/src/test/java/com/google/android/fhir/index/ResourceIndexerTest.kt +++ b/engine/src/test/java/com/google/android/fhir/index/ResourceIndexerTest.kt @@ -45,6 +45,7 @@ import org.hl7.fhir.r4.model.DecimalType import org.hl7.fhir.r4.model.Device import org.hl7.fhir.r4.model.Encounter import org.hl7.fhir.r4.model.Enumerations +import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.HumanName import org.hl7.fhir.r4.model.Identifier import org.hl7.fhir.r4.model.InstantType @@ -76,11 +77,13 @@ import org.robolectric.annotation.Config @Config(sdk = [Build.VERSION_CODES.P]) class ResourceIndexerTest { + private val resourceIndexer = ResourceIndexer(SearchParamDefinitionsProviderImpl()) + /** Unit tests for resource indexer */ @Test fun index_id() { val patient = Patient().apply { id = "3f511720-43c4-451a-830b-7f4817c619fb" } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat(resourceIndices.tokenIndices) .contains(TokenIndex("_id", "Patient.id", null, "3f511720-43c4-451a-830b-7f4817c619fb")) } @@ -93,7 +96,7 @@ class ResourceIndexerTest { meta = Meta().setLastUpdated(InstantType("2001-09-01T23:09:09.000+05:30").value) } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat(resourceIndices.dateTimeIndices) .contains( @@ -113,7 +116,7 @@ class ResourceIndexerTest { id = "non-null-ID" meta = Meta().setProfile(mutableListOf(CanonicalType("Profile/lipid"))) } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat(resourceIndices.referenceIndices) .contains(ReferenceIndex("_profile", "Patient.meta.profile", "Profile/lipid")) } @@ -125,7 +128,7 @@ class ResourceIndexerTest { id = "non-null-ID" meta = Meta().setProfile(mutableListOf(CanonicalType(""))) } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat(resourceIndices.referenceIndices.any { it.name == "_profile" }).isFalse() } @@ -138,7 +141,7 @@ class ResourceIndexerTest { id = "non-null-ID" meta = Meta().setTag(mutableListOf(Coding(systemString, codeString, "display"))) } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat(resourceIndices.tokenIndices) .contains(TokenIndex("_tag", "Patient.meta.tag", systemString, codeString)) @@ -151,7 +154,7 @@ class ResourceIndexerTest { id = "non-null-ID" meta = Meta().setTag(mutableListOf(Coding("", "", ""))) } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat(resourceIndices.tokenIndices.any { it.name == "_tag" }).isFalse() } @@ -165,7 +168,7 @@ class ResourceIndexerTest { referenceSeq.windowStart = value } - val resourceIndices = ResourceIndexer.index(molecularSequence) + val resourceIndices = resourceIndexer.index(molecularSequence) assertThat(resourceIndices.numberIndices) .contains( @@ -189,7 +192,7 @@ class ResourceIndexerTest { ) } - val resourceIndices = ResourceIndexer.index(riskAssessment) + val resourceIndices = resourceIndexer.index(riskAssessment) assertThat(resourceIndices.numberIndices) .contains(NumberIndex("probability", "RiskAssessment.prediction.probability", value)) @@ -204,7 +207,7 @@ class ResourceIndexerTest { referenceSeq = value } - val resourceIndices = ResourceIndexer.index(molecularSequence) + val resourceIndices = resourceIndexer.index(molecularSequence) assertThat(resourceIndices.numberIndices.any { it.name == "window-start" }).isFalse() assertThat( @@ -223,7 +226,7 @@ class ResourceIndexerTest { birthDate = date.value } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat(resourceIndices.dateIndices) .contains( @@ -243,7 +246,7 @@ class ResourceIndexerTest { id = "non-null-id" birthDate = null } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat(resourceIndices.dateIndices.any { it.name == "birthdate" }).isFalse() assertThat(resourceIndices.dateIndices.any { it.path == "Patient.birthDate" }).isFalse() @@ -258,7 +261,7 @@ class ResourceIndexerTest { effective = dateTime } - val resourceIndices = ResourceIndexer.index(observation) + val resourceIndices = resourceIndexer.index(observation) observation.effectiveDateTimeType assertThat(resourceIndices.dateTimeIndices) @@ -281,7 +284,7 @@ class ResourceIndexerTest { effective = instant } - val resourceIndices = ResourceIndexer.index(observation) + val resourceIndices = resourceIndexer.index(observation) assertThat(resourceIndices.dateTimeIndices) .contains( @@ -302,7 +305,7 @@ class ResourceIndexerTest { effective = period } - val resourceIndices = ResourceIndexer.index(observation) + val resourceIndices = resourceIndexer.index(observation) assertThat(resourceIndices.dateTimeIndices) .contains( @@ -328,7 +331,7 @@ class ResourceIndexerTest { effective = period } - val resourceIndices = ResourceIndexer.index(observation) + val resourceIndices = resourceIndexer.index(observation) assertThat(resourceIndices.dateTimeIndices) .contains( @@ -350,7 +353,7 @@ class ResourceIndexerTest { effective = period } - val resourceIndices = ResourceIndexer.index(observation) + val resourceIndices = resourceIndexer.index(observation) assertThat(resourceIndices.dateTimeIndices) .contains(DateTimeIndex("date", "Observation.effective", period.start.time, Long.MAX_VALUE)) @@ -370,7 +373,7 @@ class ResourceIndexerTest { effective = timing } - val resourceIndices = ResourceIndexer.index(observation) + val resourceIndices = resourceIndexer.index(observation) assertThat(resourceIndices.dateTimeIndices) .contains( @@ -400,7 +403,7 @@ class ResourceIndexerTest { effective = timing } - val resourceIndices = ResourceIndexer.index(observation) + val resourceIndices = resourceIndexer.index(observation) assertThat(resourceIndices.dateTimeIndices).isEmpty() } @@ -419,7 +422,7 @@ class ResourceIndexerTest { ) } - val resourceIndices = ResourceIndexer.index(observation) + val resourceIndices = resourceIndexer.index(observation) val dateTime = DateTimeType("2011-06-27T09:30:10+01:00") assertThat(resourceIndices.dateTimeIndices) .contains( @@ -439,7 +442,7 @@ class ResourceIndexerTest { id = "non-null-id" effective = null } - val resourceIndices = ResourceIndexer.index(observation) + val resourceIndices = resourceIndexer.index(observation) assertThat(resourceIndices.dateTimeIndices.any { it.name == "date" }).isFalse() assertThat(resourceIndices.dateTimeIndices.any { it.path == "Observation.effective" }).isFalse() @@ -454,7 +457,7 @@ class ResourceIndexerTest { addName(HumanName().addGiven(nameString)) } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat(resourceIndices.stringIndices) .contains(StringIndex("given", "Patient.name.given", nameString)) @@ -468,7 +471,7 @@ class ResourceIndexerTest { addName(HumanName().addGiven(null)) } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat( resourceIndices.stringIndices.any { stringIndex -> @@ -489,7 +492,7 @@ class ResourceIndexerTest { addName(HumanName().addGiven("")) } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat( resourceIndices.stringIndices.any { stringIndex -> @@ -509,7 +512,7 @@ class ResourceIndexerTest { deceased = BooleanType(true) } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat(resourceIndices.tokenIndices) .contains( @@ -535,7 +538,7 @@ class ResourceIndexerTest { ) } - val resourceIndices = ResourceIndexer.index(invoice) + val resourceIndices = resourceIndexer.index(invoice) assertThat(resourceIndices.tokenIndices) .contains(TokenIndex("identifier", "Invoice.identifier", system, value)) @@ -551,7 +554,7 @@ class ResourceIndexerTest { code = CodeableConcept().addCoding(Coding().setCode(codeString).setSystem(systemString)) } - val resourceIndices = ResourceIndexer.index(observation) + val resourceIndices = resourceIndexer.index(observation) assertThat(resourceIndices.tokenIndices) .contains(TokenIndex("code", "Observation.code", systemString, codeString)) @@ -571,7 +574,7 @@ class ResourceIndexerTest { display = "Display" } } - val resourceIndices = ResourceIndexer.index(encounter) + val resourceIndices = resourceIndexer.index(encounter) assertThat(resourceIndices.tokenIndices) .contains(TokenIndex("class", "Encounter.class", systemString, codeString)) @@ -585,7 +588,7 @@ class ResourceIndexerTest { code = null } - val resourceIndices = ResourceIndexer.index(observation) + val resourceIndices = resourceIndexer.index(observation) assertThat( resourceIndices.tokenIndices.any { tokenIndex -> tokenIndex.path == "Observation.code" } @@ -603,7 +606,7 @@ class ResourceIndexerTest { code = CodeableConcept().addCoding(Coding()) } - val resourceIndices = ResourceIndexer.index(observation) + val resourceIndices = resourceIndexer.index(observation) assertThat( resourceIndices.tokenIndices.any { tokenIndex -> tokenIndex.path == "Observation.code" } @@ -622,7 +625,7 @@ class ResourceIndexerTest { managingOrganization = Reference().setReference(organizationString) } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat(resourceIndices.referenceIndices) .contains(ReferenceIndex("organization", "Patient.managingOrganization", organizationString)) @@ -645,7 +648,7 @@ class ResourceIndexerTest { this.addRelatedArtifact(relatedArtifact) } - val resourceIndices = ResourceIndexer.index(activityDefinition) + val resourceIndices = resourceIndexer.index(activityDefinition) val indexPath = "ActivityDefinition.relatedArtifact.where(type='depends-on').resource | ActivityDefinition.library" @@ -667,7 +670,7 @@ class ResourceIndexerTest { this.addAction().definition = UriType("http://action2.com") } - val resourceIndices = ResourceIndexer.index(planDefinition) + val resourceIndices = resourceIndexer.index(planDefinition) val indexPath = "PlanDefinition.action.definition" val indexName = PlanDefinition.SP_DEFINITION @@ -687,7 +690,7 @@ class ResourceIndexerTest { managingOrganization = null } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat( resourceIndices.referenceIndices.any { referenceIndex -> @@ -712,7 +715,7 @@ class ResourceIndexerTest { managingOrganization = Reference().setReference("") } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat( resourceIndices.referenceIndices.any { referenceIndex -> @@ -736,7 +739,7 @@ class ResourceIndexerTest { gender = Enumerations.AdministrativeGender.UNKNOWN } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat(resourceIndices.tokenIndices) .contains( @@ -753,7 +756,7 @@ class ResourceIndexerTest { fun index_gender_null() { val patient = Patient().apply { id = "someID" } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat(resourceIndices.tokenIndices.any { it.name == "gender" }).isFalse() } @@ -766,7 +769,7 @@ class ResourceIndexerTest { totalNet = Money().setCurrency("EU").setValue(BigDecimal.valueOf(300)) } - val resourceIndices = ResourceIndexer.index(testInvoice) + val resourceIndices = resourceIndexer.index(testInvoice) assertThat(resourceIndices.quantityIndices) .contains( @@ -788,7 +791,7 @@ class ResourceIndexerTest { instance.add(Substance.SubstanceInstanceComponent().setQuantity(Quantity(100L))) } - val resourceIndices = ResourceIndexer.index(substance) + val resourceIndices = resourceIndexer.index(substance) assertThat(resourceIndices.quantityIndices) .contains( @@ -806,7 +809,7 @@ class ResourceIndexerTest { ) } - val resourceIndices = ResourceIndexer.index(substance) + val resourceIndices = resourceIndexer.index(substance) assertThat(resourceIndices.quantityIndices) .contains( @@ -822,7 +825,7 @@ class ResourceIndexerTest { instance.add(Substance.SubstanceInstanceComponent().setQuantity(null)) } - val resourceIndices = ResourceIndexer.index(substance) + val resourceIndices = resourceIndexer.index(substance) assertThat( resourceIndices.quantityIndices.any { quantityIndex -> quantityIndex.name == "quantity" } @@ -847,7 +850,7 @@ class ResourceIndexerTest { ) } - val resourceIndices = ResourceIndexer.index(substance) + val resourceIndices = resourceIndexer.index(substance) assertThat(resourceIndices.quantityIndices) .contains( @@ -874,7 +877,7 @@ class ResourceIndexerTest { ) } - val resourceIndices = ResourceIndexer.index(substance) + val resourceIndices = resourceIndexer.index(substance) assertThat(resourceIndices.quantityIndices) .contains( @@ -896,7 +899,7 @@ class ResourceIndexerTest { url = "www.someDomainName.someDomain" } - val resourceIndices = ResourceIndexer.index(device) + val resourceIndices = resourceIndexer.index(device) assertThat(resourceIndices.uriIndices) .contains(UriIndex("url", "Device.url", "www.someDomainName.someDomain")) @@ -910,7 +913,7 @@ class ResourceIndexerTest { url = null } - val resourceIndices = ResourceIndexer.index(device) + val resourceIndices = resourceIndexer.index(device) assertThat(resourceIndices.uriIndices.any { index -> index.name == "url" }).isFalse() } @@ -923,7 +926,7 @@ class ResourceIndexerTest { url = "" } - val resourceIndices = ResourceIndexer.index(device) + val resourceIndices = resourceIndexer.index(device) assertThat(resourceIndices.uriIndices.any { index -> index.name == "url" }).isFalse() } @@ -938,7 +941,7 @@ class ResourceIndexerTest { position = Location.LocationPositionComponent(DecimalType(latitude), DecimalType(longitude)) } - val resourceIndices = ResourceIndexer.index(location) + val resourceIndices = resourceIndexer.index(location) assertThat(resourceIndices.positionIndices).contains(PositionIndex(latitude, longitude)) } @@ -951,7 +954,7 @@ class ResourceIndexerTest { position = null } - val resourceIndices = ResourceIndexer.index(location) + val resourceIndices = resourceIndexer.index(location) assertThat(resourceIndices.positionIndices).isEmpty() } @@ -963,7 +966,7 @@ class ResourceIndexerTest { TestingUtils(FhirContext.forR4().newJsonParser()) .readFromFile(Invoice::class.java, "/quantity_test_invoice.json") - val resourceIndices = ResourceIndexer.index(testInvoice) + val resourceIndices = resourceIndexer.index(testInvoice) assertThat(resourceIndices.resourceId).isEqualTo(testInvoice.logicalId) @@ -1042,7 +1045,7 @@ class ResourceIndexerTest { TestingUtils(FhirContext.forR4().newJsonParser()) .readFromFile(Questionnaire::class.java, "/uri_test_questionnaire.json") - val resourceIndices = ResourceIndexer.index(testQuestionnaire) + val resourceIndices = resourceIndexer.index(testQuestionnaire) assertThat(resourceIndices.resourceType).isEqualTo(testQuestionnaire.resourceType) @@ -1099,7 +1102,7 @@ class ResourceIndexerTest { TestingUtils(FhirContext.forR4().newJsonParser()) .readFromFile(Patient::class.java, "/date_test_patient.json") - val resourceIndices = ResourceIndexer.index(testPatient) + val resourceIndices = resourceIndexer.index(testPatient) assertThat(resourceIndices.resourceType).isEqualTo(testPatient.resourceType) @@ -1195,7 +1198,7 @@ class ResourceIndexerTest { TestingUtils(FhirContext.forR4().newJsonParser()) .readFromFile(Location::class.java, "/location-example-hl7hq.json") - val resourceIndices = ResourceIndexer.index(testLocation) + val resourceIndices = resourceIndexer.index(testLocation) assertThat(resourceIndices.resourceType).isEqualTo(testLocation.resourceType) @@ -1264,7 +1267,7 @@ class ResourceIndexerTest { ) } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat(resourceIndices.stringIndices) .containsAtLeast( StringIndex("name", "Patient.name", "Mr. Pieter van de Heuvel MSc"), @@ -1280,7 +1283,7 @@ class ResourceIndexerTest { addName(HumanName()) } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat(resourceIndices.stringIndices.any { it.name == "name" }).isFalse() assertThat(resourceIndices.stringIndices.any { it.name == "phonetic" }).isFalse() } @@ -1300,7 +1303,7 @@ class ResourceIndexerTest { ) } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat(resourceIndices.stringIndices.any { it.path == "name" }).isFalse() assertThat(resourceIndices.stringIndices.any { it.name == "phonetic" }).isFalse() } @@ -1337,7 +1340,7 @@ class ResourceIndexerTest { ) } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat(resourceIndices.stringIndices) .containsAtLeast( StringIndex("name", "Patient.name", "Prof. Dr. Pieter van de Heuvel MSc Phd"), @@ -1362,7 +1365,7 @@ class ResourceIndexerTest { ) } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat(resourceIndices.stringIndices) .contains( StringIndex( @@ -1381,7 +1384,7 @@ class ResourceIndexerTest { address = listOf(Address()) } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat(resourceIndices.stringIndices.any { it.name == "address" }).isFalse() } @@ -1402,7 +1405,7 @@ class ResourceIndexerTest { ) } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat(resourceIndices.stringIndices.any { it.name == "address" }).isFalse() } @@ -1416,7 +1419,7 @@ class ResourceIndexerTest { addName(HumanName().addGiven(givenValue)) } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat( resourceIndices.stringIndices.filter { @@ -1442,7 +1445,7 @@ class ResourceIndexerTest { ) } - val resourceIndices = ResourceIndexer.index(molecularSequence) + val resourceIndices = resourceIndexer.index(molecularSequence) assertThat( resourceIndices.numberIndices.filter { @@ -1475,7 +1478,7 @@ class ResourceIndexerTest { ) } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat( resourceIndices.tokenIndices.filter { @@ -1518,7 +1521,7 @@ class ResourceIndexerTest { instance.add(Substance.SubstanceInstanceComponent().setQuantity(Quantity(values[2]))) } - val resourceIndices = ResourceIndexer.index(substance) + val resourceIndices = resourceIndexer.index(substance) assertThat( resourceIndices.quantityIndices.filter { @@ -1542,7 +1545,7 @@ class ResourceIndexerTest { addGeneralPractitioner(Reference().apply { reference = values[1] }) } - val resourceIndices = ResourceIndexer.index(patient) + val resourceIndices = resourceIndexer.index(patient) assertThat( resourceIndices.referenceIndices.filter { @@ -1574,7 +1577,7 @@ class ResourceIndexerTest { // The indexer creates 2 QuantityIndex per valueQuantity in this particular example because each // Observation.component.value can be indexed for both [Observation.SP_COMPONENT_VALUE_QUANTITY] // and [Observation.SP_COMBO_VALUE_QUANTITY] - val resourceIndices = ResourceIndexer.index(observation) + val resourceIndices = resourceIndexer.index(observation) assertThat(resourceIndices.quantityIndices) .containsExactly( @@ -1621,6 +1624,77 @@ class ResourceIndexerTest { ) } + @Test + fun index_custom_search_param() { + val patient = + Patient().apply { + addIdentifier( + Identifier().apply { + system = "https://custom-identifier-namespace" + value = "OfficialIdentifier_DarcySmith_0001" + } + ) + addName( + HumanName().apply { + use = HumanName.NameUse.OFFICIAL + family = "Smith" + addGiven("Darcy") + gender = Enumerations.AdministrativeGender.FEMALE + birthDateElement = DateType("1970-01-01") + } + ) + addExtension( + Extension().apply { + url = "http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName" + setValue(StringType("Marca")) + } + ) + } + + val resourceIndices = + ResourceIndexer( + SearchParamDefinitionsProviderImpl( + customParams = + mapOf( + "Patient" to + listOf( + SearchParamDefinition( + name = "mothers-maiden-name", + type = Enumerations.SearchParamType.STRING, + path = + "Patient.extension('http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName').value.as(String)" + ), + SearchParamDefinition( + name = "identifierPartial", + type = Enumerations.SearchParamType.STRING, + path = "Patient.identifier.value" + ) + ) + ) + ) + ) + .index(patient) + + assertThat(resourceIndices.stringIndices) + .containsExactly( + StringIndex( + name = "mothers-maiden-name", + path = + "Patient.extension('http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName').value.as(String)", + value = "Marca" + ), + StringIndex( + name = "identifierPartial", + path = "Patient.identifier.value", + value = "OfficialIdentifier_DarcySmith_0001" + ), + StringIndex(name = "family", path = "Patient.name.family", value = "Smith"), + StringIndex(name = "name", path = "Patient.name", value = "Darcy Smith"), + StringIndex(name = "phonetic", path = "Patient.name", value = "Darcy Smith"), + StringIndex(name = "given", path = "Patient.name.given", value = "Darcy") + ) + } + private companion object { // See: https://www.hl7.org/fhir/valueset-currencies.html const val FHIR_CURRENCY_SYSTEM = "urn:iso:std:iso:4217"