Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rename install API to index for file in knowledge manager #2634

Merged
merged 19 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,19 @@ import androidx.room.Room
import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.parser.IParser
import com.google.android.fhir.knowledge.db.KnowledgeDatabase
import com.google.android.fhir.knowledge.db.entities.ImplementationGuideEntity
import com.google.android.fhir.knowledge.db.entities.ResourceMetadataEntity
import com.google.android.fhir.knowledge.db.entities.toEntity
import com.google.android.fhir.knowledge.files.NpmFileManager
import com.google.android.fhir.knowledge.npm.NpmPackageDownloader
import com.google.android.fhir.knowledge.npm.OkHttpNpmPackageDownloader
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.hl7.fhir.instance.model.api.IBaseResource
import org.hl7.fhir.r4.context.IWorkerContext
import org.hl7.fhir.r4.context.SimpleWorkerContext
import org.hl7.fhir.r4.model.IdType
import org.hl7.fhir.r4.model.MetadataResource
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType
import org.hl7.fhir.utilities.npm.NpmPackage
import timber.log.Timber
Expand All @@ -46,9 +43,20 @@ import timber.log.Timber
* individually as JSON files or from FHIR NPM packages.
*
* Coordinates the management of knowledge artifacts by using the three following components:
* - database: indexing knowledge artifacts stored in the local file system,
* - file manager: managing files containing the knowledge artifacts, and
* - NPM downloader: downloading from an NPM package server the knowledge artifacts.
* - knowledgeDatabase: indexing knowledge artifacts stored in the local file system,
* - npmFileManager: managing files containing the knowledge artifacts, and
* - npmPackageDownloader: downloading the knowledge artifacts from an NPM package server .
*
* Knowledge artifacts are scoped by the application. Multiple applications using the knowledge
* manager will not share the same sets of knowledge artifacts.
*
* See [Clinical Reasoning](https://hl7.org/fhir/R4/clinicalreasoning-module.html) for the formal
* definition of knowledge artifacts. In this implementation, however, knowledge artifacts are
* represented as [MetadataResource]s.
*
* **Note** that the list of resources implementing the [MetadataResource] class differs from the
* list of resources implementing the
* [MetadataResource interface](https://www.hl7.org/fhir/R5/metadataresource.html) in FHIR R5.
*/
class KnowledgeManager
internal constructor(
Expand All @@ -60,9 +68,11 @@ internal constructor(
private val knowledgeDao = knowledgeDatabase.knowledgeDao()

/**
* Checks if the [fhirNpmPackages] 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).
* Downloads and installs the [fhirNpmPackages] from the NPM package server with transitive
* dependencies. The NPM packages will be unzipped to a directory managed by the knowledge
* manager. The resources will be indexed in the database for future retrieval.
*
* FHIR NPM packages already present in the database will be skipped.
*/
suspend fun install(vararg fhirNpmPackages: FhirNpmPackage) {
fhirNpmPackages
Expand All @@ -81,7 +91,7 @@ internal constructor(
try {
val localFhirNpmPackageMetadata =
npmFileManager.getLocalFhirNpmPackageMetadata(it.name, it.version)
install(it, localFhirNpmPackageMetadata.rootDirectory)
import(it, localFhirNpmPackageMetadata.rootDirectory)
install(*localFhirNpmPackageMetadata.dependencies.toTypedArray())
} catch (e: Exception) {
Timber.w("Unable to install package ${it.name} ${it.version}")
Expand All @@ -90,35 +100,70 @@ internal constructor(
}

/**
* Checks if the [fhirNpmPackage] is present in DB. If necessary, populates the database with the
* metadata of FHIR Resource from the provided [rootDirectory].
* Imports the content of the [fhirNpmPackage] from the provided [rootDirectory] by indexing the
* metadata of the FHIR resources for future retrieval.
*
* FHIR NPM packages already present in the database will be skipped.
*/
suspend fun install(fhirNpmPackage: FhirNpmPackage, rootDirectory: File) {
suspend fun import(fhirNpmPackage: FhirNpmPackage, rootDirectory: File) {
// TODO(ktarasenko) copy files to the safe space?
val igId = knowledgeDao.insert(fhirNpmPackage.toEntity(rootDirectory))
rootDirectory.listFiles()?.sorted()?.forEach { file ->
try {
val resource = jsonParser.parseResource(FileInputStream(file))
if (resource is Resource) {
val newId = indexResourceFile(igId, resource, file)
resource.setId(IdType(resource.resourceType.name, newId))

// Overrides the Id in the file
FileOutputStream(file).use {
it.write(jsonParser.encodeResourceToString(resource).toByteArray())
}
} else {
Timber.d("Unable to import file: %file")
}
} catch (exception: Exception) {
Timber.d(exception, "Unable to import file: %file")
}
val implementationGuideId =
knowledgeDao.insert(
ImplementationGuideEntity(
0L,
fhirNpmPackage.canonical ?: "",
fhirNpmPackage.name,
fhirNpmPackage.version,
rootDirectory,
),
)
val files = rootDirectory.listFiles() ?: return
files.sorted().forEach { file ->
// Ignore files that are not meta resources instead of throwing exceptions since unzipped
// NPM package might contain other types of files e.g. package.json.
val resource = readMetadataResourceOrNull(file) ?: return@forEach
knowledgeDao.insertResource(
implementationGuideId,
ResourceMetadataEntity(
0,
resource.resourceType,
resource.url,
resource.name,
resource.version,
file,
),
)
}
}

/** Imports the Knowledge Artifact from the provided [file] to the default dependency. */
suspend fun install(file: File) {
importFile(null, file)
/**
* Indexes a knowledge artifact as a JSON object in the provided [file].
*
* This creates a record of the knowledge artifact's metadata and the file's location. When the
* knowledge artifact is requested, knowledge manager will load the content of the file,
* deserialize it and return the resulting FHIR resource.
*
* This operation does not make a copy of the knowledge artifact, nor does it checksum the content
* of the file. Therefore, it cannot be guaranteed that subsequent retrievals of the knowledge
* artifact will produce the same result. Applications using this function must be aware of the
* risk of the content of the file being modified or corrupt, potentially resulting in incorrect
* or inaccurate result of decision support or measure evaluation.
*
* Use this API for knowledge artifacts in immutable files (e.g. in the app's `assets` folder).
*/
suspend fun index(file: File) {
val resource = readMetadataResourceOrThrow(file)
knowledgeDao.insertResource(
null,
ResourceMetadataEntity(
0L,
resource.resourceType,
resource.url,
resource.name,
resource.version,
file,
),
)
}

/** Loads resources from IGs listed in dependencies. */
Expand All @@ -141,7 +186,7 @@ internal constructor(
name != null -> knowledgeDao.getResourcesWithName(resType, name)
else -> knowledgeDao.getResources(resType)
}
return resourceEntities.map { loadResource(it) }
return resourceEntities.map { readMetadataResourceOrThrow(it.resourceFile)!! }
}

/** Deletes Implementation Guide, cleans up files. */
Expand All @@ -155,43 +200,6 @@ internal constructor(
}
}

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 -> {
val newId = indexResourceFile(igId, resource, file)
resource.setId(IdType(resource.resourceType.name, newId))

// Overrides the Id in the file
FileOutputStream(file).use {
it.write(jsonParser.encodeResourceToString(resource).toByteArray())
}
}
}
}

private suspend fun indexResourceFile(igId: Long?, resource: Resource, file: File): Long {
val metadataResource = resource as? MetadataResource
val res =
ResourceMetadataEntity(
0L,
resource.resourceType,
metadataResource?.url,
metadataResource?.name,
metadataResource?.version,
file,
)

return knowledgeDao.insertResource(igId, res)
}

/**
* Loads and initializes a worker context with the specified npm packages.
*
Expand Down Expand Up @@ -220,8 +228,37 @@ internal constructor(
}
}

private fun loadResource(resourceEntity: ResourceMetadataEntity): IBaseResource {
return jsonParser.parseResource(FileInputStream(resourceEntity.resourceFile))
/**
* Parses and returns the content of a file containing a FHIR resource in JSON, or null if the
* file does not contain a FHIR resource.
*/
private suspend fun readResourceOrNull(file: File): IBaseResource? =
withContext(Dispatchers.IO) {
try {
FileInputStream(file).use(jsonParser::parseResource)
} catch (e: Exception) {
Timber.e(e, "Unable to load resource from $file")
null
}
}

/**
* Parses and returns the content of a file containing a FHIR metadata resource in JSON, or null
* if the file does not contain a FHIR metadata resource.
*/
private suspend fun readMetadataResourceOrNull(file: File) =
readResourceOrNull(file) as? MetadataResource
jingtang10 marked this conversation as resolved.
Show resolved Hide resolved

/**
* Parses and returns the content of a file containing a FHIR metadata resource in JSON, or throws
* an exception if the file does not contain a FHIR metadata resource.
*/
private suspend fun readMetadataResourceOrThrow(file: File): MetadataResource {
val resource = readResourceOrNull(file)!!
check(resource is MetadataResource) {
"Resource ${resource.idElement} is not a MetadataResource"
}
return resource
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 Google LLC
* Copyright 2022-2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -19,7 +19,6 @@ package com.google.android.fhir.knowledge.db.entities
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import com.google.android.fhir.knowledge.FhirNpmPackage
import java.io.File

/**
Expand All @@ -45,7 +44,3 @@ internal data class ImplementationGuideEntity(
/** Directory where the Implementation Guide files are stored */
val rootDirectory: File,
)

internal fun FhirNpmPackage.toEntity(rootFolder: File): ImplementationGuideEntity {
return ImplementationGuideEntity(0L, canonical ?: "", name, version, rootFolder)
}
Loading
Loading