Skip to content

Commit

Permalink
Implement a simple NpmManager (#2028)
Browse files Browse the repository at this point in the history
* Add npm packager manager classes

* Rename .java to .kt

* Remove GsonDependency, remove rednundant classes

* Import downloaded ig

* Import downloaded ig

* Add SimplePackageCacheManager

* Remove npmpackage from npmpackagemanager

* Add npm manager

* Implement NpmManager

* Rollback version updates

* Address review suggestion:
- Rename ImplementationGuide.kt to Dependency.kt
- Drop the NpmPackageManager.kt

* Spottless apply

* Fix tests

* Use  current module in workflow library

* Run spotless apply

* Remove print statement

* Use kotlin stream functions

* Remove dead code

* Remove dead code

* Move FhirOperatorBuilder into FhirOperator.kt file

* Address comments and renaming

---------

Co-authored-by: Jing Tang <[email protected]>
  • Loading branch information
ktarasenko and jingtang10 authored Oct 4, 2023
1 parent 6b43d65 commit ddb2a7f
Show file tree
Hide file tree
Showing 22 changed files with 654 additions and 109 deletions.
4 changes: 4 additions & 0 deletions buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ object Dependencies {
"$androidFhirGroup:$androidFhirEngineModule:${Versions.androidFhirEngine}"
const val androidFhirKnowledge = "$androidFhirGroup:knowledge:${Versions.androidFhirKnowledge}"

const val apacheCommonsCompress =
"org.apache.commons:commons-compress:${Versions.apacheCommonsCompress}"

const val desugarJdkLibs = "com.android.tools:desugar_jdk_libs:${Versions.desugarJdkLibs}"
const val fhirUcum = "org.fhir:ucum:${Versions.fhirUcum}"
const val gson = "com.google.code.gson:gson:${Versions.gson}"
Expand Down Expand Up @@ -243,6 +246,7 @@ object Dependencies {
const val androidFhirCommon = "0.1.0-alpha04"
const val androidFhirEngine = "0.1.0-beta03"
const val androidFhirKnowledge = "0.1.0-alpha01"
const val apacheCommonsCompress = "1.21"
const val desugarJdkLibs = "2.0.3"
const val caffeine = "2.9.1"
const val fhirUcum = "1.0.3"
Expand Down
6 changes: 5 additions & 1 deletion knowledge/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,14 @@ dependencies {
coreLibraryDesugaring(Dependencies.desugarJdkLibs)

implementation(Dependencies.Kotlin.stdlib)
implementation(Dependencies.Kotlin.kotlinCoroutinesCore)
implementation(Dependencies.Lifecycle.liveDataKtx)
implementation(Dependencies.Room.ktx)
implementation(Dependencies.Room.runtime)
implementation(Dependencies.timber)
implementation(Dependencies.Kotlin.kotlinCoroutinesCore)
implementation(Dependencies.http)
implementation(Dependencies.HapiFhir.fhirCoreConvertors)
implementation(Dependencies.apacheCommonsCompress)

kapt(Dependencies.Room.compiler)

Expand All @@ -100,6 +103,7 @@ dependencies {
testImplementation(Dependencies.Kotlin.kotlinCoroutinesTest)
testImplementation(Dependencies.mockitoInline)
testImplementation(Dependencies.mockitoKotlin)
testImplementation(Dependencies.mockWebServer)
testImplementation(Dependencies.robolectric)
testImplementation(Dependencies.truth)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2023 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.knowledge

/**
* A FHIR NPM Package as defined by the FHIR specification.
*
* See https://hl7.org/fhir/packages.html for the published FHIR NPM Packages specification.
*
* See https://confluence.hl7.org/display/FHIR/NPM+Package+Specification for more info under the
* management of FHIR Infrastructure.
*/
data class FhirNpmPackage(val name: String, val version: String, val canonical: String? = null)
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import ca.uhn.fhir.parser.IParser
import com.google.android.fhir.knowledge.db.impl.KnowledgeDatabase
import com.google.android.fhir.knowledge.db.impl.entities.ResourceMetadataEntity
import com.google.android.fhir.knowledge.db.impl.entities.toEntity
import com.google.android.fhir.knowledge.npm.NpmFileManager
import com.google.android.fhir.knowledge.npm.OkHttpPackageDownloader
import com.google.android.fhir.knowledge.npm.PackageDownloader
import java.io.File
import java.io.FileInputStream
import kotlinx.coroutines.Dispatchers
Expand All @@ -37,27 +40,41 @@ import timber.log.Timber
class KnowledgeManager
internal constructor(
private val knowledgeDatabase: KnowledgeDatabase,
dataFolder: File,
private val jsonParser: IParser = FhirContext.forR4().newJsonParser(),
private val npmFileManager: NpmFileManager =
NpmFileManager(File(dataFolder, ".fhir_package_cache")),
private val packageDownloader: PackageDownloader = OkHttpPackageDownloader(npmFileManager),
) {

private val knowledgeDao = knowledgeDatabase.knowledgeDao()

/**
* * 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)
* 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).
*/
suspend fun install(vararg implementationGuides: ImplementationGuide) {
TODO("[1937]Not implemented yet ")
suspend fun install(vararg fhirNpmPackages: FhirNpmPackage) {
fhirNpmPackages
.filter { knowledgeDao.getImplementationGuide(it.name, it.version) == null }
.forEach {
val npmPackage =
if (npmFileManager.containsPackage(it.name, it.version)) {
npmFileManager.getPackage(it.name, it.version)
} else {
packageDownloader.downloadPackage(it, PACKAGE_SERVER)
}
install(it, npmPackage.rootDirectory)
install(*npmPackage.dependencies.toTypedArray())
}
}

/**
* Checks if the [implementationGuide] is present in DB. If necessary, populates the database with
* the metadata of FHIR Resource from the provided [rootDirectory].
* Checks if the [fhirNpmPackage] is present in DB. If necessary, populates the database with the
* metadata of FHIR Resource from the provided [rootDirectory].
*/
suspend fun install(implementationGuide: ImplementationGuide, rootDirectory: File) {
suspend fun install(fhirNpmPackage: FhirNpmPackage, rootDirectory: File) {
// TODO(ktarasenko) copy files to the safe space?
val igId = knowledgeDao.insert(implementationGuide.toEntity(rootDirectory))
val igId = knowledgeDao.insert(fhirNpmPackage.toEntity(rootDirectory))
rootDirectory.listFiles()?.forEach { file ->
try {
val resource = jsonParser.parseResource(FileInputStream(file))
Expand Down Expand Up @@ -101,10 +118,9 @@ internal constructor(
}

/** Deletes Implementation Guide, cleans up files. */
suspend fun delete(vararg igDependencies: ImplementationGuide) {
suspend fun delete(vararg igDependencies: FhirNpmPackage) {
igDependencies.forEach { igDependency ->
val igEntity =
knowledgeDao.getImplementationGuide(igDependency.packageId, igDependency.version)
val igEntity = knowledgeDao.getImplementationGuide(igDependency.name, igDependency.version)
if (igEntity != null) {
knowledgeDao.deleteImplementationGuide(igEntity)
igEntity.rootDirectory.deleteRecursively()
Expand Down Expand Up @@ -150,15 +166,20 @@ internal constructor(

companion object {
private const val DB_NAME = "knowledge.db"
private const val PACKAGE_SERVER = "https://packages.fhir.org/packages/"

/** Creates an [KnowledgeManager] backed by the Room DB. */
fun create(context: Context) =
KnowledgeManager(
Room.databaseBuilder(context, KnowledgeDatabase::class.java, DB_NAME).build(),
context.dataDir,
)

/** Creates an [KnowledgeManager] backed by the in-memory DB. */
fun createInMemory(context: Context) =
KnowledgeManager(Room.inMemoryDatabaseBuilder(context, KnowledgeDatabase::class.java).build())
KnowledgeManager(
Room.inMemoryDatabaseBuilder(context, KnowledgeDatabase::class.java).build(),
context.dataDir,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package com.google.android.fhir.knowledge.db.impl.entities
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import com.google.android.fhir.knowledge.ImplementationGuide
import com.google.android.fhir.knowledge.FhirNpmPackage
import java.io.File

/**
Expand All @@ -46,6 +46,6 @@ internal data class ImplementationGuideEntity(
val rootDirectory: File,
)

internal fun ImplementationGuide.toEntity(rootFolder: File): ImplementationGuideEntity {
return ImplementationGuideEntity(0L, uri, packageId, version, rootFolder)
internal fun FhirNpmPackage.toEntity(rootFolder: File): ImplementationGuideEntity {
return ImplementationGuideEntity(0L, canonical ?: "", name, version, rootFolder)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Google LLC
* Copyright 2023 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 @@ -14,10 +14,16 @@
* limitations under the License.
*/

package com.google.android.fhir.knowledge
package com.google.android.fhir.knowledge.npm

/**
* Holds Implementation Guide attributes. Used to define dependencies, load dependencies from
* Package Manager
*/
data class ImplementationGuide(val packageId: String, val version: String, val uri: String)
import com.google.android.fhir.knowledge.FhirNpmPackage
import java.io.File

/** Downloaded FHIR NPM Package metadata. */
data class LocalFhirNpmPackageMetadata(
val packageId: String,
val version: String,
val canonical: String?,
val dependencies: List<FhirNpmPackage>,
val rootDirectory: File,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2023 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.knowledge.npm

import com.google.android.fhir.knowledge.FhirNpmPackage
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject

/** Manages stored NPM packages. */
internal class NpmFileManager(private val cacheRoot: File) {

/**
* Returns the NpmPackage for the given [packageId] and [version] from cache or `null` if the
* package is not cached.
*/
suspend fun getPackage(packageId: String, version: String): LocalFhirNpmPackageMetadata {
return withContext(Dispatchers.IO) {
val packageFolder = File(getPackageFolder(packageId, version), "package")
readNpmPackage(packageFolder)
}
}

/**
* Returns the NpmPackage for the given [packageId] and [version] from cache or `null` if the
* package is not cached.
*/
suspend fun containsPackage(packageId: String, version: String): Boolean {
return withContext(Dispatchers.IO) {
val packageFolder = File(getPackageFolder(packageId, version), "package")
val packageJson = File(packageFolder, "package.json")
packageJson.exists()
}
}

/** Returns the package folder for the given [packageId] and [version]. */
fun getPackageFolder(packageId: String, version: String) = File(cacheRoot, "$packageId#$version")

/** Creates an [LocalFhirNpmPackageMetadata] parsing the package manifest file. */
private fun readNpmPackage(packageFolder: File): LocalFhirNpmPackageMetadata {
val packageJson = File(packageFolder, "package.json")
val json = JSONObject(packageJson.readText())
with(json) {
val dependenciesList = optJSONObject("dependencies")
val dependencies =
dependenciesList?.keys()?.asSequence()?.map { key ->
FhirNpmPackage(key, dependenciesList.getString(key))
}

return LocalFhirNpmPackageMetadata(
getString("name"),
getString("version"),
optString("canonical"),
dependencies?.toList() ?: emptyList(),
packageFolder,
)
}
}
}
Loading

0 comments on commit ddb2a7f

Please sign in to comment.