Skip to content

Commit

Permalink
ELM JSON reader and writer (Kotlin feature branch) (#1489)
Browse files Browse the repository at this point in the history
* JSON serializers for QName and BigDecimal. Initial cleanup.

* Fix Sonar warnings

---------

Co-authored-by: Jonathan Percival <[email protected]>
  • Loading branch information
antvaset and JPercival authored Jan 16, 2025
1 parent b5a62e8 commit 4479b47
Show file tree
Hide file tree
Showing 20 changed files with 141 additions and 98 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ void elmTests() {
}

@Test
@Disabled("TODO: Re-enable once XmlUtil-based ELM JSON deserialization is implemented")
@Disabled("TODO: Re-enable once XmlUtil-based ELM JSON deserialization is implemented for annotations")
void jsonANCFHIRDummyLibraryLoad() {
try {
final Library library = deserializeJsonLibrary("ElmDeserialize/ANCFHIRDummy.json");
Expand Down Expand Up @@ -65,7 +65,6 @@ void jsonANCFHIRDummyLibraryLoad() {
}

@Test
@Disabled("TODO: Re-enable once XmlUtil-based ELM JSON deserialization is implemented")
void jsonAdultOutpatientEncountersFHIR4LibraryLoad() {
try {
final Library library =
Expand Down Expand Up @@ -145,7 +144,7 @@ void xmlLibraryLoad() {
}

@Test
@Disabled("TODO: Re-enable once XmlUtil-based ELM JSON deserialization is implemented")
@Disabled("TODO: Re-enable once XmlUtil-based ELM JSON deserialization is implemented for annotations")
void jsonTerminologyLibraryLoad() {
try {
final Library library = deserializeJsonLibrary("ElmDeserialize/ANCFHIRTerminologyDummy.json");
Expand Down Expand Up @@ -328,12 +327,10 @@ void emptyStringsTest() throws IOException {
new org.cqframework.cql.elm.serializing.xmlutil.ElmXmlLibraryReader().read(new StringReader(xml));
validateEmptyStringsTest(xmlLibrary);

// TODO: Re-enable once XmlUtil-based ELM JSON deserialization is implemented
// String json = toJson(translator.toELM());
// Library jsonLibrary =
// new org.cqframework.cql.elm.serializing.xmlutil.ElmJsonLibraryReader().read(new
// StringReader(json));
// validateEmptyStringsTest(jsonLibrary);
String json = toJson(translator.toELM());
Library jsonLibrary =
new org.cqframework.cql.elm.serializing.xmlutil.ElmJsonLibraryReader().read(new StringReader(json));
validateEmptyStringsTest(jsonLibrary);
}

private static Library deserializeJsonLibrary(String filePath) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.cqframework.cql.elm.serializing.xmlutil

import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.plus
import kotlinx.serialization.modules.serializersModuleOf
import org.hl7.elm_modelinfo.r1.serializing.BigDecimalJsonSerializer

val json = Json {
serializersModule =
serializersModuleOf(BigDecimalJsonSerializer) +
org.hl7.elm.r1.serializersModule +
org.hl7.cql_annotations.r1.serializersModule
explicitNulls = false
ignoreUnknownKeys = true
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,11 @@ import java.io.Reader
import java.net.URI
import java.net.URL
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.modules.plus
import org.cqframework.cql.elm.serializing.ElmLibraryReader
import org.hl7.elm.r1.Library

class ElmJsonLibraryReader : ElmLibraryReader {
val module =
org.hl7.elm.r1.Serializer.createSerializer() +
org.hl7.cql_annotations.r1.Serializer.createSerializer()
val json = Json {
serializersModule = module
explicitNulls = false
ignoreUnknownKeys = true
}

override fun read(file: File): Library {
file.inputStream().use {
return read(it)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package org.cqframework.cql.elm.serializing.xmlutil

import java.io.Writer
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.plus
import org.cqframework.cql.elm.serializing.ElmLibraryWriter
import org.hl7.elm.r1.Library

Expand All @@ -12,14 +10,6 @@ class ElmJsonLibraryWriter : ElmLibraryWriter {
}

override fun writeAsString(library: Library): String {
val module =
org.hl7.elm.r1.Serializer.createSerializer() +
org.hl7.cql_annotations.r1.Serializer.createSerializer()
val json = Json {
serializersModule = module
explicitNulls = false
}

return json.encodeToString(LibraryWrapper.serializer(), LibraryWrapper(library))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.cqframework.cql.elm.serializing.xmlutil

import kotlinx.serialization.modules.plus
import kotlinx.serialization.modules.serializersModuleOf
import nl.adaptivity.xmlutil.QName
import nl.adaptivity.xmlutil.serialization.XML
import org.hl7.elm_modelinfo.r1.serializing.BigDecimalXmlSerializer

val xml =
XML(
serializersModuleOf(BigDecimalXmlSerializer) +
org.hl7.elm.r1.serializersModule +
org.hl7.cql_annotations.r1.serializersModule
) {
xmlDeclMode = nl.adaptivity.xmlutil.XmlDeclMode.Charset
defaultPolicy {
typeDiscriminatorName =
QName("http://www.w3.org/2001/XMLSchema-instance", "type", "xsi")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import java.io.InputStream
import java.io.Reader
import java.net.URI
import java.net.URL
import kotlinx.serialization.modules.plus
import nl.adaptivity.xmlutil.serialization.XML
import nl.adaptivity.xmlutil.xmlStreaming
import org.cqframework.cql.elm.serializing.ElmLibraryReader
import org.hl7.elm.r1.Library
Expand Down Expand Up @@ -45,11 +43,6 @@ class ElmXmlLibraryReader : ElmLibraryReader {
}

override fun read(reader: Reader): Library {
val serializersModule =
org.hl7.elm.r1.Serializer.createSerializer() +
org.hl7.cql_annotations.r1.Serializer.createSerializer()
val xml = XML(serializersModule)

return xml.decodeFromReader(Library.serializer(), xmlStreaming.newReader(reader))
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package org.cqframework.cql.elm.serializing.xmlutil

import java.io.Writer
import kotlinx.serialization.modules.plus
import nl.adaptivity.xmlutil.QName
import nl.adaptivity.xmlutil.serialization.XML
import org.cqframework.cql.elm.serializing.ElmLibraryWriter
import org.hl7.elm.r1.Library

Expand All @@ -13,18 +10,6 @@ class ElmXmlLibraryWriter : ElmLibraryWriter {
}

override fun writeAsString(library: Library): String {
val serializersModule =
org.hl7.elm.r1.Serializer.createSerializer() +
org.hl7.cql_annotations.r1.Serializer.createSerializer()

val xml =
XML(serializersModule) {
xmlDeclMode = nl.adaptivity.xmlutil.XmlDeclMode.Charset
defaultPolicy {
typeDiscriminatorName =
QName("http://www.w3.org/2001/XMLSchema-instance", "type", "xsi")
}
}

return xml.encodeToString(Library.serializer(), library)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,10 @@
import org.cqframework.cql.cql2elm.LibraryManager;
import org.cqframework.cql.cql2elm.ModelManager;
import org.json.JSONException;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.skyscreamer.jsonassert.JSONAssert;

@Disabled(
"""
Currently failing due to differences in QName serialization.
The default QName serializer expects a structured object, not a string
Possible fix: Custom serializer for QName.""")
class CMS146JsonTest {

private static Object[][] sigFileAndSigLevel() {
Expand All @@ -47,7 +41,17 @@ void cms146SignatureLevels(String fileName, SignatureLevel expectedSignatureLeve
new LibraryManager(
modelManager, new CqlCompilerOptions(ErrorSeverity.Warning, expectedSignatureLevel)));
final String jsonWithVersion = translator.toJson();
final String actualJson = jsonWithVersion.replaceAll("\"translatorVersion\":\"[^\"]*\",", "");
final String actualJson = jsonWithVersion
.replaceAll("\"translatorVersion\":\"[^\"]*\",", "")
// The original JSON marshaller (JAXB + MOXy) does not output
// accessLevel if it is null. (It always emits it otherwise,
// even when it's set to the default value.) The new JSON
// serializer always emits accessLevel.
// We do not set accessLevel in the translator on the
// model/context def node, thus the difference in JSON output.
.replace(
"\"name\":\"Patient\",\"context\":\"Patient\",\"accessLevel\":\"Public\"",
"\"name\":\"Patient\",\"context\":\"Patient\"");
JSONAssert.assertEquals(expectedJson, actualJson, true);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,13 @@ void cms146SignatureLevels(String fileName, SignatureLevel expectedSignatureLeve
// temporary fix for namespace prefix differences
// .replaceAll("xmlns:n1=\"urn:hl7-org:elm:r1\"", "")
// .replaceAll("n1:", "")
// Possible bug in original XML, no access modifier on when name and context are both Patient?
// Maybe it's not emitting default access modifiers?

// The original XML marshaller (JAXB) does not output
// accessLevel if it is null. (It always emits it otherwise,
// even when it's set to the default value.) The new XML
// serializer (XmlUtil) always emits accessLevel.
// We do not set accessLevel in the translator on the
// model/context def node, thus the difference in XML output.
.replace(
"name=\"Patient\" context=\"Patient\" accessLevel=\"Public\"",
"name=\"Patient\" context=\"Patient\"");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

public class ElmXmlutilTest {
Expand All @@ -18,8 +17,6 @@ void deserializeReserializeElmJson() {
}

@Test
@Disabled(
"TODO: Polymorphic serializer for class org.hl7.elm.r1.ChoiceTypeSpecifier (Kotlin reflection is not available) has property 'type' that conflicts with JSON class discriminator. You can either change class discriminator in JsonConfiguration, rename property with @SerialName annotation or fall back to array polymorphism")
void deserializeBigElmJson() {
var lib = new ElmJsonLibraryReader()
.read(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import org.hl7.elm_modelinfo.r1.*
import org.hl7.elm_modelinfo.r1.ModelInfo
import org.hl7.elm_modelinfo.r1.serializing.ModelInfoReader

val xml = XML(org.hl7.elm_modelinfo.r1.serializersModule)

class XmlModelInfoReader : ModelInfoReader {
override fun read(source: Source): ModelInfo {
val serializersModule = Serializer.createSerializer()
val xml = XML(serializersModule)
val modelInfo =
xml.decodeFromReader(
ModelInfo.serializer(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,29 @@ import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*

object BigDecimalSerializer : KSerializer<BigDecimal?> {
object BigDecimalXmlSerializer : KSerializer<BigDecimal> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("BigDecimal", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: BigDecimal?) {
encoder.encodeString(value?.toPlainString() ?: "")
override fun serialize(encoder: Encoder, value: BigDecimal) {
encoder.encodeString(value.toPlainString())
}

override fun deserialize(decoder: Decoder): BigDecimal {
return BigDecimal(decoder.decodeString())
return decoder.decodeString().toBigDecimal()
}
}

// We use JSON numbers, not strings to serialize BigDecimals in JSON.
object BigDecimalJsonSerializer : KSerializer<BigDecimal> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("BigDecimal", PrimitiveKind.DOUBLE)

override fun serialize(encoder: Encoder, value: BigDecimal) {
encoder.encodeDouble(value.toDouble())
}

override fun deserialize(decoder: Decoder): BigDecimal {
return decoder.decodeDouble().toBigDecimal()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.hl7.elm_modelinfo.r1.serializing

import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import nl.adaptivity.xmlutil.*


@OptIn(ExperimentalXmlUtilApi::class)
object QNameJsonSerializer : XmlSerializer<QName> by QNameSerializer {
@OptIn(XmlUtilInternal::class)
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("javax.xml.namespace.QName", PrimitiveKind.STRING).xml(
PrimitiveSerialDescriptor("javax.xml.namespace.QName", PrimitiveKind.STRING),
QName(XMLConstants.XSD_NS_URI, "QName", XMLConstants.XSD_PREFIX)
)

override fun serialize(encoder: Encoder, value: QName) {
encoder.encodeString(
value.toString()
)
}

override fun deserialize(decoder: Decoder): QName {
return QName.valueOf(decoder.decodeString())
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ public ModelInfo load(ModelIdentifier modelIdentifier) {
if (isQDMModelIdentifier(modelIdentifier)) {
String localVersion = modelIdentifier.getVersion() == null ? "" : modelIdentifier.getVersion();
var stream = getQdmResource(localVersion);
return ModelInfoReaderFactory.INSTANCE.getReader("application/xml").read(buffered(asSource(stream)));
if (stream != null) {
return ModelInfoReaderFactory.INSTANCE
.getReader("application/xml")
.read(buffered(asSource(stream)));
}
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ public ModelInfo load(ModelIdentifier modelIdentifier) {
if (isFHIRModelIdentifier(modelIdentifier)) {
String localVersion = modelIdentifier.getVersion() == null ? "" : modelIdentifier.getVersion();
var stream = getResource(localVersion);
return ModelInfoReaderFactory.INSTANCE.getReader("application/xml").read(buffered(asSource(stream)));
if (stream != null) {
return ModelInfoReaderFactory.INSTANCE
.getReader("application/xml")
.read(buffered(asSource(stream)));
}
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ public ModelInfo load(ModelIdentifier modelIdentifier) {
if (isQICoreModelIdentifier(modelIdentifier)) {
String localVersion = modelIdentifier.getVersion() == null ? "" : modelIdentifier.getVersion();
var stream = getResource(localVersion);
return ModelInfoReaderFactory.INSTANCE.getReader("application/xml").read(buffered(asSource(stream)));
if (stream != null) {
return ModelInfoReaderFactory.INSTANCE
.getReader("application/xml")
.read(buffered(asSource(stream)));
}
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ public ModelInfo load(ModelIdentifier modelIdentifier) {
if (isQuickFhirModelIdentifier(modelIdentifier)) {
String localVersion = modelIdentifier.getVersion() == null ? "" : modelIdentifier.getVersion();
var stream = getResource(localVersion);
return ModelInfoReaderFactory.INSTANCE.getReader("application/xml").read(buffered(asSource(stream)));
if (stream != null) {
return ModelInfoReaderFactory.INSTANCE
.getReader("application/xml")
.read(buffered(asSource(stream)));
}
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ public ModelInfo load(ModelIdentifier modelIdentifier) {
if (isQuickModelIdentifier(modelIdentifier)) {
String localVersion = modelIdentifier.getVersion() == null ? "" : modelIdentifier.getVersion();
var stream = getResource(localVersion);
return ModelInfoReaderFactory.INSTANCE.getReader("application/xml").read(buffered(asSource(stream)));
if (stream != null) {
return ModelInfoReaderFactory.INSTANCE
.getReader("application/xml")
.read(buffered(asSource(stream)));
}
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ public ModelInfo load(ModelIdentifier modelIdentifier) {
if (isUSCoreModelIdentifier(modelIdentifier)) {
String localVersion = modelIdentifier.getVersion() == null ? "" : modelIdentifier.getVersion();
var stream = getResource(localVersion);
return ModelInfoReaderFactory.INSTANCE.getReader("application/xml").read(buffered(asSource(stream)));
if (stream != null) {
return ModelInfoReaderFactory.INSTANCE
.getReader("application/xml")
.read(buffered(asSource(stream)));
}
}

return null;
Expand Down
Loading

0 comments on commit 4479b47

Please sign in to comment.