Skip to content

Commit

Permalink
Ig manager integration with the workflow library (#1854)
Browse files Browse the repository at this point in the history
* Integrate IgManager

* Address review comments

* Address review comments

* Address review comments

* Address review comments

* Workaround #1920 : lookup by id in IgManager

* Address review comments

* Make implementationGuide field in the db optional, remove the concept of the "default ig" and use `null` instead
  • Loading branch information
ktarasenko committed Apr 4, 2023
1 parent 96af7f1 commit 0d1f71e
Show file tree
Hide file tree
Showing 44 changed files with 14,352 additions and 230 deletions.
1 change: 1 addition & 0 deletions benchmark/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ dependencies {
androidTestImplementation(Dependencies.truth)

androidTestImplementation(project(":engine"))
androidTestImplementation(project(":implementationguide"))
androidTestImplementation(project(":workflow"))
androidTestImplementation(project(":workflow-testing"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,22 @@

package com.google.android.fhir.benchmark

import android.content.Context
import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.context.FhirVersionEnum
import com.google.android.fhir.FhirEngineProvider
import com.google.android.fhir.workflow.FhirOperator
import com.google.android.fhir.implementationguide.IgManager
import com.google.android.fhir.workflow.FhirOperatorBuilder
import com.google.common.truth.Truth.assertThat
import java.io.File
import java.io.InputStream
import kotlinx.coroutines.runBlocking
import org.hl7.fhir.r4.model.Bundle
import org.hl7.fhir.r4.model.Library
import org.hl7.fhir.r4.model.Parameters
import org.junit.Rule
import org.junit.Test
Expand All @@ -48,20 +52,30 @@ class G_CqlEvaluatorBenchmark {
val fhirOperator = runWithTimingDisabled {
val fhirContext = FhirContext.forCached(FhirVersionEnum.R4)
val jsonParser = fhirContext.newJsonParser()
val context: Context = ApplicationProvider.getApplicationContext()

val patientImmunizationHistory =
jsonParser.parseResource(open("/immunity-check/ImmunizationHistory.json")) as Bundle
val fhirEngine = FhirEngineProvider.getInstance(ApplicationProvider.getApplicationContext())
val igManager = IgManager.createInMemory(context)
val lib = jsonParser.parseResource(open("/immunity-check/ImmunityCheck.json")) as Library

runBlocking {
for (entry in patientImmunizationHistory.entry) {
fhirEngine.create(entry.resource)
}
igManager.install(
File(context.filesDir, lib.name).apply {
writeText(jsonParser.encodeResourceToString(lib))
}
)
}

val lib = jsonParser.parseResource(open("/immunity-check/ImmunityCheck.json")) as Bundle

FhirOperator(fhirContext, fhirEngine).also { it.loadLibs(lib) }
FhirOperatorBuilder(context)
.withFhirContext(fhirContext)
.withFhirEngine(fhirEngine)
.withIgManager(igManager)
.build()
}

val results =
Expand Down
4 changes: 2 additions & 2 deletions implementationguide/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ android {
}

sourceSets {
getByName("androidTest").apply { resources.setSrcDirs(listOf("sampledata")) }
getByName("androidTest").apply { resources.setSrcDirs(listOf("testdata")) }

getByName("test").apply { resources.setSrcDirs(listOf("sampledata")) }
getByName("test").apply { resources.setSrcDirs(listOf("testdata")) }
}

buildTypes {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,27 @@ import com.google.android.fhir.implementationguide.db.impl.entities.ResourceMeta
import com.google.android.fhir.implementationguide.db.impl.entities.toEntity
import java.io.File
import java.io.FileInputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.hl7.fhir.instance.model.api.IBaseResource
import org.hl7.fhir.r4.model.MetadataResource
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType
import timber.log.Timber

/** Responsible for importing, accessing and deleting Implementation Guides. */
class IgManager internal constructor(igDatabase: ImplementationGuideDatabase) {
class IgManager internal constructor(private val igDatabase: ImplementationGuideDatabase) {

private val igDao = igDatabase.implementationGuideDao()
private val jsonParser = FhirContext.forR4().newJsonParser()

/**
* * Checks if the [igDependencies] are present in DB. If necessary, downloads the dependencies
* from NPM and imports data from the package manager (populates the metadata of the FHIR
* Resorces)
* * Checks if the [implementationGuides] are present in DB. If necessary, downloads the
* dependencies from NPM and imports data from the package manager (populates the metadata of the
* FHIR Resources)
*/
suspend fun install(vararg igDependencies: ImplementationGuide) {
TODO("not implemented yet")
suspend fun install(vararg implementationGuides: ImplementationGuide) {
TODO("[1937]Not implemented yet ")
}

/**
Expand All @@ -68,20 +70,22 @@ class IgManager internal constructor(igDatabase: ImplementationGuideDatabase) {

/** Imports the IG from the provided [file] to the default dependency. */
suspend fun install(file: File) {
TODO("not implemented yet")
importFile(null, file)
}

/** Loads resources from IGs listed in dependencies. */
suspend fun loadResources(
resourceType: String,
url: String? = null,
id: String? = null,
name: String? = null,
version: String? = null,
): Iterable<IBaseResource> {
val resType = ResourceType.fromCode(resourceType)
val resourceEntities =
when {
url != null -> listOfNotNull(igDao.getResourceWithUrl(url))
id != null -> listOfNotNull(igDao.getResourceWithUrlLike("%$id"))
name != null && version != null ->
listOfNotNull(igDao.getResourcesWithNameAndVersion(resType, name, version))
name != null -> igDao.getResourcesWithName(resType, name)
Expand All @@ -94,12 +98,28 @@ class IgManager internal constructor(igDatabase: ImplementationGuideDatabase) {
suspend fun delete(vararg igDependencies: ImplementationGuide) {
igDependencies.forEach { igDependency ->
val igEntity = igDao.getImplementationGuide(igDependency.packageId, igDependency.version)
igDao.deleteImplementationGuide(igEntity)
igEntity.rootDirectory.deleteRecursively()
if (igEntity != null) {
igDao.deleteImplementationGuide(igEntity)
igEntity.rootDirectory.deleteRecursively()
}
}
}

private suspend fun importFile(igId: Long?, file: File) {
val resource =
withContext(Dispatchers.IO) {
try {
FileInputStream(file).use(jsonParser::parseResource)
} catch (exception: Exception) {
Timber.d(exception, "Unable to import file: $file. Parsing to FhirResource failed.")
}
}
when (resource) {
is Resource -> importResource(igId, resource, file)
}
}

private suspend fun importResource(igId: Long, resource: Resource, file: File) {
private suspend fun importResource(igId: Long?, resource: Resource, file: File) {
val metadataResource = resource as? MetadataResource
val res =
ResourceMetadataEntity(
Expand All @@ -117,6 +137,10 @@ class IgManager internal constructor(igDatabase: ImplementationGuideDatabase) {
return jsonParser.parseResource(FileInputStream(resourceEntity.resourceFile))
}

fun close() {
igDatabase.close()
}

companion object {
private const val DB_NAME = "implementationguide.db"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ abstract class ImplementationGuideDao {

@Transaction
internal open suspend fun insertResource(
implementationGuideId: Long,
implementationGuideId: Long?,
resource: ResourceMetadataEntity,
) {

Expand All @@ -47,14 +47,16 @@ abstract class ImplementationGuideDao {
// exception if they
// are different.
val resourceMetadataId = resourceMetadata?.resourceMetadataId ?: insert(resource)
insert(ImplementationGuideResourceMetadataEntity(implementationGuideId, resourceMetadataId))
insert(ImplementationGuideResourceMetadataEntity(0, implementationGuideId, resourceMetadataId))
}

@Transaction
internal open suspend fun deleteImplementationGuide(name: String, version: String) {
val igEntity = getImplementationGuide(name, version)
deleteImplementationGuide(igEntity)
deleteOrphanedResources()
if (igEntity != null) {
deleteImplementationGuide(igEntity)
deleteOrphanedResources()
}
}

@Query(
Expand All @@ -71,7 +73,7 @@ abstract class ImplementationGuideDao {
internal abstract suspend fun getImplementationGuide(
packageId: String,
version: String?,
): ImplementationGuideEntity
): ImplementationGuideEntity?

@Query("SELECT * from ResourceMetadataEntity")
internal abstract suspend fun getResources(): List<ResourceMetadataEntity>
Expand All @@ -85,6 +87,11 @@ abstract class ImplementationGuideDao {
internal abstract suspend fun getResourceWithUrl(
url: String,
): ResourceMetadataEntity?
// Remove after https://github.com/google/android-fhir/issues/1920
@Query("SELECT * from ResourceMetadataEntity WHERE url LIKE :urlPart")
internal abstract suspend fun getResourceWithUrlLike(
urlPart: String,
): ResourceMetadataEntity?

@Query(
"SELECT * from ResourceMetadataEntity WHERE resourceType = :resourceType AND name = :name"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,17 @@ import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.Junction
import androidx.room.PrimaryKey
import androidx.room.Relation

/** Connects [ImplementationGuideEntity] to [ResourceMetadataEntity]. */
@Entity(
primaryKeys = ["implementationGuideId", "resourceMetadataId"],
indices = [Index(value = ["implementationGuideId"]), Index(value = ["resourceMetadataId"])],
indices =
[
Index(value = ["implementationGuideId"]),
Index(value = ["resourceMetadataId"]),
Index(value = ["implementationGuideId", "resourceMetadataId"], unique = true)
],
foreignKeys =
[
ForeignKey(
Expand All @@ -44,7 +49,8 @@ import androidx.room.Relation
]
)
internal data class ImplementationGuideResourceMetadataEntity(
val implementationGuideId: Long,
@PrimaryKey(autoGenerate = true) val id: Long,
val implementationGuideId: Long?,
val resourceMetadataId: Long,
)

Expand All @@ -55,5 +61,5 @@ internal data class ImplementationGuideWithResources(
entityColumn = "resourceMetadataId",
associateBy = Junction(ImplementationGuideResourceMetadataEntity::class)
)
val resources: List<ResourceMetadataEntity>
val resources: List<ResourceMetadataEntity>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ internal data class ResourceMetadataEntity(
@PrimaryKey(autoGenerate = true) val resourceMetadataId: Long,
val resourceType: ResourceType,
val url: String?,
val version: String?,
val name: String?,
val version: String?,
/** Location of the JSON file with a full Resource. */
val resourceFile: File,
)
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,16 @@ internal class IgManagerTest {
@Test
fun `importing IG creates entries in DB`() = runTest {
igManager.install(implementationGuide, dataFolder)
val implementationGuideId =
igDb
.implementationGuideDao()
.getImplementationGuide("anc-cds", "0.3.0")!!
.implementationGuideId

assertThat(
igDb
.implementationGuideDao()
.getImplementationGuidesWithResources(
igDb
.implementationGuideDao()
.getImplementationGuide("anc-cds", "0.3.0")
.implementationGuideId
)
.getImplementationGuidesWithResources(implementationGuideId)
?.resources
)
.hasSize(6)
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"contained": [ {
"resourceType": "RequestGroup",
"id": "MedRequest-Example",
"instantiatesCanonical":[
"http:https://localhost/PlanDefinition/MedRequest-Example"
],
"status": "draft",
"intent": "proposal",
"subject": {
Expand All @@ -24,6 +27,9 @@
"reference": "Patient/Patient-Example"
}
} ],
"instantiatesCanonical": [
"http:https://localhost/PlanDefinition/MedRequest-Example"
],
"status": "draft",
"intent": "proposal",
"subject": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"resource": {
"resourceType" : "PlanDefinition",
"id" : "MedRequest-Example",
"url" : "http:https://localhost/PlanDefinition/MedRequest-Example",
"title" : "This example illustrates a medication request",
"status" : "active",
"action" : [{
Expand Down
8 changes: 8 additions & 0 deletions workflow/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ android {
testInstrumentationRunnerArguments["package"] = "com.google.android.fhir.workflow"
}

sourceSets {
getByName("androidTest").apply { resources.setSrcDirs(listOf("sampledata")) }

getByName("test").apply { resources.setSrcDirs(listOf("sampledata")) }
}

// Added this for fixing out of memory issue in running test cases
tasks.withType<Test>().configureEach {
maxParallelForks = (Runtime.getRuntime().availableProcessors() - 1).takeIf { it > 0 } ?: 1
Expand Down Expand Up @@ -110,6 +116,7 @@ dependencies {
implementation(Dependencies.Cql.translatorElmJackson) // Necessary to import XML/JSON CQL Libs
implementation(Dependencies.Cql.translatorModel) // Overrides HAPI's old versions
implementation(Dependencies.Cql.translatorModelJackson) // Necessary to import XML/JSON ModelInfos
implementation(Dependencies.timber)

// Forces the most recent version of jackson, ignoring what dependencies use.
// Remove these lines when HAPI 6.4 becomes available.
Expand All @@ -131,6 +138,7 @@ dependencies {
implementation(Dependencies.Kotlin.stdlib)
implementation(Dependencies.xerces)
implementation(project(":engine"))
implementation(project(":implementationguide"))

testImplementation(Dependencies.AndroidxTest.core)
testImplementation(Dependencies.jsonAssert)
Expand Down
Loading

0 comments on commit 0d1f71e

Please sign in to comment.