Skip to content

Commit

Permalink
Merge pull request #5 from d-tree-org/dev
Browse files Browse the repository at this point in the history
v0.0.5
  • Loading branch information
sevenreup authored Nov 12, 2024
2 parents f8989cb + a7ece6e commit a6aea32
Show file tree
Hide file tree
Showing 78 changed files with 5,242 additions and 128 deletions.
15 changes: 15 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
KEYCLOAK_ID=
KEYCLOAK_SECRET=
KEYCLOAK_ISSUER=
KEYCLOAK_AUTHORIZE_TOKEN_URL=
KEYCLOAK_ACCESS_TOKEN_URL=
KEYCLOAK_USERNAME=
KEYCLOAK_PASSWORD=
FHIR_BASE_URL=
COOLIFY_TOKEN=

RESOURCES_CACHE_DIR=
RESOURCES_CACHE_TAG=
RESOURCES_CACHE_URL=
RESOURCES_SSH_PASSPHRASE=
RESOURCES_SSH_LOCATION=
6 changes: 3 additions & 3 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

- name: Deploy to Coolify
run: |
curl --request GET '${{ secrets.COOLIFY_WEBHOOK }}' --header 'Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}'
# - name: Deploy to Coolify
# run: |
# curl --request GET '${{ secrets.COOLIFY_WEBHOOK }}' --header 'Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}'
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,9 @@ bin/
### Mac OS ###
.DS_Store

*.env
*.env

.kotlin/

.temp/
fhir_data/
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
run:
docker-compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .dev.env up -d

down:
docker-compose -f docker-compose.yml -f docker-compose.dev.yml down
13 changes: 13 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
tasks.register("downloadDependencies") {
doLast {
allprojects {
configurations.all {
try {
resolve()
} catch (e: Exception) {
// Ignore resolution errors
}
}
}
}
}
3 changes: 3 additions & 0 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ dependencies {
testImplementation(kotlin("test"))
testImplementation(libs.junit.jupiter.api)
testRuntimeOnly(libs.junit.jupiter.engine)

// https://mvnrepository.com/artifact/org.json/json
implementation("org.json:json:20240303")
}


Expand Down
46 changes: 46 additions & 0 deletions core/src/main/kotlin/com/google/android/fhir/LocalChange.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package com.google.android.fhir

import ca.uhn.fhir.parser.IParser
import org.dtree.fhir.core.uploader.ContentTypes
import org.hl7.fhir.r4.model.Binary
import org.hl7.fhir.r4.model.Bundle
import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent
import org.hl7.fhir.r4.model.Resource
import java.time.Instant

data class LocalChange(
Expand All @@ -20,6 +26,7 @@ data class LocalChange(
* [LocalChange] class instance is created.
*/
var token: LocalChangeToken,
val isPatch: Boolean = false,
) {
enum class Type(val value: Int) {
INSERT(1), // create a new resource. payload is the entire resource json.
Expand All @@ -31,6 +38,45 @@ data class LocalChange(
fun from(input: Int): Type = values().first { it.value == input }
}
}

fun createPatchRequest(
iParser: IParser,
resource: Resource? = null
): BundleEntryComponent {
return if (type == LocalChange.Type.UPDATE) {
if (isPatch) {
createRequest(createPathRequest())
} else {
createRequest(iParser.parseResource(payload) as Resource)
}
} else if (resource != null && type == LocalChange.Type.INSERT) {
createRequest(resource)
} else {
val resourceToUpload = iParser.parseResource(payload) as Resource
createRequest(resourceToUpload)
}
}

private fun createPathRequest(): Binary {
return Binary().apply {
contentType = ContentTypes.APPLICATION_JSON_PATCH
data = payload.toByteArray()
}
}

private fun createRequest(resourceToUpload: Resource): BundleEntryComponent {
return BundleEntryComponent().apply {
resource = resourceToUpload
request = Bundle.BundleEntryRequestComponent().apply {
url = "${resourceType}/${resourceId}"
method = when (type) {
LocalChange.Type.INSERT -> Bundle.HTTPVerb.PUT
LocalChange.Type.UPDATE -> Bundle.HTTPVerb.PATCH
LocalChange.Type.DELETE -> Bundle.HTTPVerb.DELETE
}
}
}
}
}

data class LocalChangeToken(val ids: List<Long>)
146 changes: 146 additions & 0 deletions core/src/main/kotlin/com/google/android/fhir/MoreTypes.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Copyright 2023-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.
* 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 org.hl7.fhir.r4.model.CodeType
import org.hl7.fhir.r4.model.Coding
import org.hl7.fhir.r4.model.Expression
import org.hl7.fhir.r4.model.IdType
import org.hl7.fhir.r4.model.PrimitiveType
import org.hl7.fhir.r4.model.Quantity
import org.hl7.fhir.r4.model.StringType
import org.hl7.fhir.r4.model.Type
import org.hl7.fhir.r4.model.UriType
import java.util.*


/**
* Returns the string representation for [PrimitiveType] or [Quantity], otherwise defaults to null
*/
private fun getValueString(type: Type): String? =
when (type) {
is Quantity -> type.value?.toString()
else -> (type as? PrimitiveType<*>)?.asStringValue()
}

/** Converts StringType to toUriType. */
internal fun StringType.toUriType(): UriType {
return UriType(value)
}

/** Converts StringType to CodeType. */
internal fun StringType.toCodeType(): CodeType {
return CodeType(value)
}

/** Converts StringType to IdType. */
internal fun StringType.toIdType(): IdType {
return IdType(value)
}

/** Converts Coding to CodeType. */
internal fun Coding.toCodeType(): CodeType {
return CodeType(code)
}

/**
* Converts Quantity to Coding type. The resulting Coding properties are equivalent of Coding.system
* = Quantity.system Coding.code = Quantity.code Coding.display = Quantity.unit
*/
internal fun Quantity.toCoding(): Coding {
return Coding(this.system, this.code, this.unit)
}

/**
* Returns whether two instances of the [Type] class are equal.
*
* Note this is not an operator because it is not possible to overload the equality operator as an
* extension.
*/
fun equals(a: Type, b: Type): Boolean {
if (a::class != b::class) return false

if (a === b) return true

if (a.isPrimitive) return a.primitiveValue() == b.primitiveValue()

// Codes with the same system and code values are considered equal even if they have different
// display values.
if (a is Coding && b is Coding) return a.system == b.system && a.code == b.code

throw NotImplementedError("Comparison for type ${a::class.java} not supported.")
}

internal fun Type.hasValue(): Boolean = !getValueString(this).isNullOrBlank()

internal val Type.cqfCalculatedValueExpression
get() = this.getExtensionByUrl(EXTENSION_CQF_CALCULATED_VALUE_URL)?.value as? Expression

internal const val EXTENSION_CQF_CALCULATED_VALUE_URL: String =
"http://hl7.org/fhir/StructureDefinition/cqf-calculatedValue"

operator fun Type.compareTo(value: Type): Int {
if (!this.fhirType().equals(value.fhirType())) {
throw IllegalArgumentException(
"Cannot compare different data types: ${this.fhirType()} and ${value.fhirType()}",
)
}
when {
this.fhirType().equals("integer") -> {
return this.primitiveValue().toInt().compareTo(value.primitiveValue().toInt())
}
this.fhirType().equals("decimal") -> {
return this.primitiveValue().toBigDecimal().compareTo(value.primitiveValue().toBigDecimal())
}
this.fhirType().equals("date") -> {
return clearTimeFromDateValue(this.dateTimeValue().value)
.compareTo(clearTimeFromDateValue(value.dateTimeValue().value))
}
this.fhirType().equals("dateTime") -> {
return this.dateTimeValue().value.compareTo(value.dateTimeValue().value)
}
this.fhirType().equals("Quantity") -> {
val quantity =
UnitConverter.getCanonicalFormOrOriginal(UcumValue((this as Quantity).code, this.value))
val anotherQuantity =
UnitConverter.getCanonicalFormOrOriginal(UcumValue((value as Quantity).code, value.value))
if (quantity.code != anotherQuantity.code) {
throw IllegalArgumentException(
"Cannot compare different quantity codes: ${quantity.code} and ${anotherQuantity.code}",
)
}
return quantity.value.compareTo(anotherQuantity.value)
}
else -> {
throw NotImplementedError()
}
}
}

private fun clearTimeFromDateValue(dateValue: Date): Date {
val calendarValue = Calendar.getInstance()
calendarValue.time = dateValue
calendarValue.set(Calendar.HOUR_OF_DAY, 0)
calendarValue.set(Calendar.MINUTE, 0)
calendarValue.set(Calendar.SECOND, 0)
calendarValue.set(Calendar.MILLISECOND, 0)
return calendarValue.time
}

fun StringType.getLocalizedText(lang: String = Locale.getDefault().toLanguageTag()): String? {
return getTranslation(lang) ?: getTranslation(lang.split("-").firstOrNull()) ?: value
}
90 changes: 90 additions & 0 deletions core/src/main/kotlin/com/google/android/fhir/UnitConverter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* 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

import java.lang.NullPointerException
import java.math.BigDecimal
import java.math.MathContext
import org.fhir.ucum.Decimal
import org.fhir.ucum.Pair
import org.fhir.ucum.UcumEssenceService
import org.fhir.ucum.UcumException

/**
* Canonicalizes unit values to UCUM base units.
*
* For details of UCUM, see http://unitsofmeasure.org/
*
* For using UCUM with FHIR, see https://www.hl7.org/fhir/ucum.html
*
* For the implementation of UCUM with FHIR, see https://github.com/FHIR/Ucum-java
*/
object UnitConverter {
private val ucumService by lazy {
UcumEssenceService(this::class.java.getResourceAsStream("/ucum-essence.xml"))
}

/**
* Returns the canonical form of a UCUM Value.
*
* The canonical form is generated by normalizing [value] to UCUM base units, used to generate
* canonical matches on Quantity Search
*
* @throws ConverterException if fails to generate canonical matches
*
* For example a value of 1000 mm will return 1 m.
*/
fun getCanonicalForm(value: UcumValue): UcumValue {
try {
val pair =
ucumService.getCanonicalForm(Pair(Decimal(value.value.toPlainString()), value.code))
return UcumValue(
pair.code,
pair.value.asDecimal().toBigDecimal(MathContext(value.value.precision())),
)
} catch (e: UcumException) {
throw ConverterException("UCUM conversion failed", e)
} catch (e: NullPointerException) {
// See https://github.com/google/android-fhir/issues/869 for why NPE needs to be caught
throw ConverterException("Missing numerical value in the canonical UCUM value", e)
}
}

/**
* Returns the canonical form of a UCUM Value if it is supported in Ucum library.
*
* The canonical form is generated by normalizing [value] to UCUM base units, used to generate
* canonical matches on Quantity Search, if fails to generate then returns original value
*
* For example a value of 1000 mm will return 1 m.
*/
fun getCanonicalFormOrOriginal(value: UcumValue): UcumValue {
return try {
getCanonicalForm(value)
} catch (e: ConverterException) {
val pair = Pair(Decimal(value.value.toPlainString()), value.code)
UcumValue(
pair.code,
pair.value.asDecimal().toBigDecimal(MathContext(value.value.precision())),
)
}
}
}

class ConverterException(message: String, cause: Throwable) : Exception(message, cause)

data class UcumValue(val code: String, val value: BigDecimal)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.google.android.fhir.datacapture

import org.hl7.fhir.r4.model.Resource

/**
* Resolves resources based on the provided xFhir query. This allows the library to resolve
* x-fhir-query answer expressions.
*
* NOTE: The result of the resolution may be cached to improve performance. In other words, the
* resolver may be called only once after which the Resources may be used multiple times in the UI.
*/
fun interface XFhirQueryResolver {
suspend fun resolve(xFhirQuery: String): List<Resource>
}
Loading

0 comments on commit a6aea32

Please sign in to comment.