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

Implement a simple NpmManager #2028

Merged
merged 26 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
702527c
Add npm packager manager classes
ktarasenko Nov 19, 2022
dd8810e
Rename .java to .kt
ktarasenko Mar 24, 2023
11d209f
Remove GsonDependency, remove rednundant classes
ktarasenko Mar 24, 2023
5e0a25c
Import downloaded ig
ktarasenko Apr 4, 2023
6aa6d6a
Import downloaded ig
ktarasenko Apr 16, 2023
8e65792
Add SimplePackageCacheManager
ktarasenko Apr 16, 2023
9d2545a
Remove npmpackage from npmpackagemanager
ktarasenko Apr 17, 2023
60957a6
Add npm manager
ktarasenko May 21, 2023
dcbc44a
Implement NpmManager
ktarasenko Jun 7, 2023
a19a64e
Rollback version updates
ktarasenko Jun 15, 2023
627130e
Address review suggestion:
ktarasenko Jul 14, 2023
a6f47d6
Spottless apply
ktarasenko Jul 21, 2023
b518f01
Fix tests
ktarasenko Aug 16, 2023
df2a6a6
Use current module in workflow library
ktarasenko Aug 16, 2023
d32aa95
Merge branch 'master' into pr/2028
jingtang10 Sep 27, 2023
469e536
Run spotless apply
jingtang10 Sep 27, 2023
84327b7
Remove print statement
jingtang10 Sep 27, 2023
79fe4f3
Use kotlin stream functions
jingtang10 Sep 27, 2023
ab9942f
Merge branch 'master' into ig-manager-npm
jingtang10 Sep 27, 2023
4b59c66
Remove dead code
jingtang10 Sep 27, 2023
c1d8a01
Remove dead code
jingtang10 Sep 27, 2023
9269d02
Move FhirOperatorBuilder into FhirOperator.kt file
jingtang10 Sep 27, 2023
fb89822
Address comments and renaming
jingtang10 Oct 4, 2023
7de93b0
Merge branch 'master' into ig-manager-npm
jingtang10 Oct 4, 2023
99d9ea0
Fix tests
jingtang10 Oct 4, 2023
9e31ab3
Fix test
jingtang10 Oct 4, 2023
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
5 changes: 4 additions & 1 deletion buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ object Dependencies {
"$androidFhirGroup:$androidFhirEngineModule:${Versions.androidFhirEngine}"
const val androidFhirKnowledge = "$androidFhirGroup:knowledge:${Versions.androidFhirKnowledge}"

const val apacheCommonsCompress =
jingtang10 marked this conversation as resolved.
Show resolved Hide resolved
"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 @@ -259,6 +262,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 All @@ -269,7 +273,6 @@ object Dependencies {
// Newer versions of HapiFhir don't work on Android due to the use of Caffeine 3+
// Wait for this to release (6.3): https://github.com/hapifhir/hapi-fhir/pull/4196
const val hapiFhir = "6.0.1"

// Newer versions don't work on Android due to Apache Commons Codec:
// Wait for this fix: https://github.com/hapifhir/org.hl7.fhir.core/issues/1046
const val hapiFhirCore = "5.6.36"
Expand Down
6 changes: 5 additions & 1 deletion knowledge/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,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 @@ -104,6 +107,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
@@ -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 @@ -20,4 +20,4 @@ package com.google.android.fhir.knowledge
* 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)
data class Dependency(val packageId: String, val version: String, val uri: String? = null)
jingtang10 marked this conversation as resolved.
Show resolved Hide resolved
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,42 @@ 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 [dependencies] 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 dependencies: Dependency) {
for (dependency in dependencies) {
if (knowledgeDao.getImplementationGuide(dependency.packageId, dependency.version) != null)
continue
jingtang10 marked this conversation as resolved.
Show resolved Hide resolved
println(dependency)
jingtang10 marked this conversation as resolved.
Show resolved Hide resolved
val containsPackage = npmFileManager.containsPackage(dependency.packageId, dependency.version)
val npmPackage =
if (containsPackage) {
npmFileManager.getPackage(dependency.packageId, dependency.version)
} else {
packageDownloader.downloadPackage(dependency, PACKAGE_SERVER)
jingtang10 marked this conversation as resolved.
Show resolved Hide resolved
}
install(dependency, 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 [dependency] 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(dependency: Dependency, rootDirectory: File) {
// TODO(ktarasenko) copy files to the safe space?
val igId = knowledgeDao.insert(implementationGuide.toEntity(rootDirectory))
val igId = knowledgeDao.insert(dependency.toEntity(rootDirectory))
rootDirectory.listFiles()?.forEach { file ->
try {
val resource = jsonParser.parseResource(FileInputStream(file))
Expand Down Expand Up @@ -101,7 +119,7 @@ internal constructor(
}

/** Deletes Implementation Guide, cleans up files. */
suspend fun delete(vararg igDependencies: ImplementationGuide) {
suspend fun delete(vararg igDependencies: Dependency) {
igDependencies.forEach { igDependency ->
val igEntity =
knowledgeDao.getImplementationGuide(igDependency.packageId, igDependency.version)
Expand Down Expand Up @@ -150,15 +168,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()
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
@@ -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 @@ -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.Dependency
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 Dependency.toEntity(rootFolder: File): ImplementationGuideEntity {
jingtang10 marked this conversation as resolved.
Show resolved Hide resolved
return ImplementationGuideEntity(0L, uri ?: "", packageId, version, rootFolder)
}
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.Dependency
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): NpmPackage {
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 [NpmPackage] parsing the package manifest file. */
private fun readNpmPackage(packageFolder: File): NpmPackage {
val packageJson = File(packageFolder, "package.json")
val json = JSONObject(packageJson.readText())
with(json) {
val dependenciesList = optJSONObject("dependencies")
val dependencies =
dependenciesList?.keys()?.asSequence()?.map { key ->
Dependency(key, dependenciesList.getString(key))
}

return NpmPackage(
getString("name"),
getString("version"),
optString("canonical"),
dependencies?.toList() ?: emptyList(),
packageFolder
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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.Dependency
import java.io.File

/** Downloaded Npm Package metadata. */
data class NpmPackage(
val packageId: String,
val version: String,
val canonical: String?,
val dependencies: List<Dependency>,
val rootDirectory: File,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* 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.Dependency
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.util.zip.GZIPInputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.ResponseBody
import org.apache.commons.compress.archivers.tar.TarArchiveEntry
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream

/** Downloads Npm package from the provided package server using OkHttp library. */
internal class OkHttpPackageDownloader(
private val npmFileManager: NpmFileManager,
) : PackageDownloader {

val client = OkHttpClient()

@Throws(IOException::class)
override suspend fun downloadPackage(
dependency: Dependency,
packageServerUrl: String,
): NpmPackage {
return withContext(Dispatchers.IO) {
val packageName = dependency.packageId
val version = dependency.version
val url = "$packageServerUrl$packageName/$version"

val request = Request.Builder().url(url).get().build()

val response = client.newCall(request).execute()

if (!response.isSuccessful) {
throw IOException("Unexpected code $response")
}
val packageFolder = npmFileManager.getPackageFolder(dependency.packageId, dependency.version)

response.body?.use { responseBody ->
packageFolder.mkdirs()
val tgzFile = File(packageFolder, "$packageName-$version.tgz")
saveResponseToFile(responseBody, tgzFile)

extractTgzFile(tgzFile, packageFolder)

tgzFile.delete()
}
npmFileManager.getPackage(dependency.packageId, dependency.version)
}
}

private fun saveResponseToFile(responseBody: ResponseBody, file: File) {
FileOutputStream(file).use { fileOutputStream ->
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytesRead: Int
while (responseBody.byteStream().read(buffer).also { bytesRead = it } != -1) {
fileOutputStream.write(buffer, 0, bytesRead)
}
}
}

private fun extractTgzFile(tgzFile: File, outputFolder: File) {
FileInputStream(tgzFile).use { fileInputStream ->
outputFolder.mkdirs()
GZIPInputStream(fileInputStream).use { gzipInputStream ->
TarArchiveInputStream(gzipInputStream).use { tarInputStream ->
var entry: TarArchiveEntry? = tarInputStream.nextTarEntry
while (entry != null) {
val outputFile = File(outputFolder, entry.name)

if (entry.isDirectory) {
outputFile.mkdirs()
} else {
outputFile.parentFile?.mkdirs()

val outputFileStream = FileOutputStream(outputFile)
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytesRead: Int
while (tarInputStream.read(buffer).also { bytesRead = it } != -1) {
outputFileStream.write(buffer, 0, bytesRead)
}
outputFileStream.close()
}

entry = tarInputStream.nextTarEntry
}
}
}
}
}
}
Loading