Skip to content

Commit

Permalink
Break out UCUM Service into its own module (#1471)
Browse files Browse the repository at this point in the history
* Break out UCUM Service into its own module

* Fix tests

* Fix exception types
  • Loading branch information
JPercival authored Dec 16, 2024
1 parent b4dd0fa commit 82823a8
Show file tree
Hide file tree
Showing 13 changed files with 128 additions and 39 deletions.
1 change: 1 addition & 0 deletions Src/java/cql-to-elm-cli/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dependencies {
implementation(project(":qdm"))
implementation(project(":model-jaxb"))
implementation(project(":elm-jaxb"))
implementation(project(":ucum"))
implementation("net.sf.jopt-simple:jopt-simple:4.7")
implementation("org.slf4j:slf4j-simple:1.7.36")
implementation("org.glassfish.jaxb:jaxb-runtime:4.0.5")
Expand Down
2 changes: 1 addition & 1 deletion Src/java/cql-to-elm/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ dependencies {
api(project(":cql"))
api(project(":model"))
api(project(":elm"))
api("org.fhir:ucum:1.0.8")

// TODO: This dependencies are required due the the fact that the CqlTranslatorOptionsMapper lives
// in the cql-to-elm project. Ideally, we"d factor out all serialization dependencies into common
Expand All @@ -17,6 +16,7 @@ dependencies {
testImplementation(project(":model-jackson"))
testImplementation(project(":quick"))
testImplementation(project(":qdm"))
testImplementation(project(":ucum"))
testImplementation("com.github.reinert:jjschema:1.16")
testImplementation("com.tngtech.archunit:archunit:1.2.1")
testImplementation("org.skyscreamer:jsonassert:1.5.1")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,11 @@ import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.collections.HashSet
import org.cqframework.cql.cql2elm.model.CompiledLibrary
import org.cqframework.cql.cql2elm.ucum.UcumService
import org.cqframework.cql.cql2elm.ucum.UcumServiceFactory
import org.cqframework.cql.elm.serializing.ElmLibraryReaderFactory
import org.fhir.ucum.UcumEssenceService
import org.fhir.ucum.UcumException
import org.fhir.ucum.UcumService
import org.hl7.cql.model.NamespaceManager
import org.hl7.elm.r1.*
import org.slf4j.Logger
import org.slf4j.LoggerFactory

/**
* Manages a set of CQL libraries. As new library references are encountered during compilation, the
Expand All @@ -39,26 +36,7 @@ constructor(
var compiledLibraries: MutableMap<VersionedIdentifier, CompiledLibrary> =
libraryCache ?: HashMap()
val librarySourceLoader: LibrarySourceLoader = PriorityLibrarySourceLoader()
var ucumService: UcumService? = null
get() {
if (field == null) {
field = defaultUcumService
}
return field
}

@get:Synchronized
private val defaultUcumService: UcumService?
get() {
try {
return UcumEssenceService(
UcumEssenceService::class.java.getResourceAsStream("/ucum-essence.xml")
)
} catch (e: UcumException) {
logger.warn("Error creating shared UcumService", e)
}
return null
}
val ucumService: UcumService by lazy { UcumServiceFactory.load() }

/*
* A "well-known" library name is one that is allowed to resolve without a
Expand Down Expand Up @@ -397,7 +375,6 @@ constructor(
}

companion object {
private val logger: Logger = LoggerFactory.getLogger(LibraryManager::class.java)
private val supportedContentTypes: Array<LibraryContentType> =
arrayOf(LibraryContentType.JSON, LibraryContentType.XML, LibraryContentType.CQL)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.cqframework.cql.cql2elm.ucum

import java.math.BigDecimal

interface UcumService {
/**
* Converts a quantity from one unit to another
*
* @param value the quantity to convert
* @param sourceUnit the unit of the quantity
* @param destUnit the unit to convert to
* @return the converted value in terms of the destination unit
*/
fun convert(value: BigDecimal, sourceUnit: String, destUnit: String): BigDecimal

/**
* Validate checks that a string is valid ucum unit
*
* @param unit
* @return null if valid, error message if invalid
*/
fun validate(unit: String): String?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.cqframework.cql.cql2elm.ucum

import java.util.*

object UcumServiceFactory {
fun load(): UcumService {
return ServiceLoader.load(UcumService::class.java).firstOrNull()
?: error(
"""No UCUM service implementation found.
Please ensure a UCUM service implementation is available on the classpath.
The 'ucum' module is a reference implementation that can be used for this purpose."""
)
}
}
1 change: 1 addition & 0 deletions Src/java/elm-jaxb/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ plugins {
dependencies {
api(project(":elm"))
testImplementation(project(":cql-to-elm"))
testImplementation(project(":ucum"))
testImplementation(project(":model-jaxb"))
}
1 change: 1 addition & 0 deletions Src/java/engine/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ plugins {
dependencies {
api(project(":elm"))
api(project(":cql-to-elm"))
api(project(":ucum"))
api("org.apache.commons:commons-text:1.10.0")

testImplementation(project(":model-jackson"))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package org.opencds.cqf.cql.engine.elm.executing;

import java.math.BigDecimal;
import org.fhir.ucum.Decimal;
import org.fhir.ucum.UcumService;
import org.cqframework.cql.cql2elm.ucum.UcumService;
import org.opencds.cqf.cql.engine.exception.InvalidOperatorArgument;
import org.opencds.cqf.cql.engine.runtime.Quantity;

Expand Down Expand Up @@ -37,13 +35,9 @@ public static Object convertQuantity(Object argument, Object unit, UcumService u
return null;
}
try {
Decimal result = ucumService.convert(
new Decimal(String.valueOf(((Quantity) argument).getValue())),
((Quantity) argument).getUnit(),
(String) unit);
return new Quantity()
.withValue(new BigDecimal(result.asDecimal()))
.withUnit((String) unit);
var result = ucumService.convert(
((Quantity) argument).getValue(), ((Quantity) argument).getUnit(), (String) unit);
return new Quantity().withValue(result).withUnit((String) unit);
} catch (Exception e) {
return null;
}
Expand Down
4 changes: 2 additions & 2 deletions Src/java/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ include(
"cql-to-elm",
"cql-to-elm-cli",
"elm-fhir",
"ucum",
"tools:cql-formatter",
"tools:cql-parsetree",
"tools:xsd-to-modelinfo"
)


include("ucum")
8 changes: 8 additions & 0 deletions Src/java/ucum/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
plugins {
id("cql.library-conventions")
}

dependencies {
api(project(":cql-to-elm"))
api("org.fhir:ucum:1.0.8")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.cqframework.cql.ucum

import org.cqframework.cql.cql2elm.ucum.UcumService
import org.fhir.ucum.Decimal
import org.fhir.ucum.UcumEssenceService

@ConsistentCopyVisibility
data class DefaultUcumService private constructor(private val ucumService: UcumService) :
UcumService by ucumService {
constructor() : this(createUcumService())

companion object {
private fun createUcumService(): UcumService {
return try {
UcumEssenceService(
UcumEssenceService::class.java.getResourceAsStream("/ucum-essence.xml")
)
.let { u ->
object : UcumService {
override fun convert(
value: java.math.BigDecimal,
sourceUnit: String,
destUnit: String
): java.math.BigDecimal {
val ucumValue = Decimal(value.toString())
val converted = u.convert(ucumValue, sourceUnit, destUnit)
return java.math.BigDecimal(converted.asDecimal())
}

override fun validate(unit: String): String? {
return u.validate(unit)
}
}
}
} catch (e: org.fhir.ucum.UcumException) {
throw IllegalStateException(
"""Failed to create UCUM service.
Please ensure the 'ucum-essence.xml' file is available on the classpath.
The 'ucum' module is a reference implementation that can be used for this purpose.""",
e
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.cqframework.cql.ucum.DefaultUcumService
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.cqframework.cql.ucum

import java.math.BigDecimal
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class DefaultUcumServiceTest {

@Test
fun testConvert() {
val ucumService = DefaultUcumService()
val result = ucumService.convert(BigDecimal("1"), "mg", "g")
assertEquals(BigDecimal("0.0010"), result)
}

@Test
fun testValidate() {
val ucumService = DefaultUcumService()
assertNull(ucumService.validate("mg"))
assertTrue(ucumService.validate("foo")?.contains("The unit 'foo' is unknown") ?: false)
}
}

0 comments on commit 82823a8

Please sign in to comment.