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]>
antvaset and JPercival authored Jan 16, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent b5a62e8 commit 4479b47
Showing 20 changed files with 141 additions and 98 deletions.
Original file line number Diff line number Diff line change
@@ -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");
@@ -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 =
@@ -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");
@@ -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 {
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
@@ -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)
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

@@ -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
@@ -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
@@ -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

@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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() {
@@ -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);
}

Original file line number Diff line number Diff line change
@@ -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\"");
Original file line number Diff line number Diff line change
@@ -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 {
@@ -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(
Original file line number Diff line number Diff line change
@@ -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(),
Original file line number Diff line number Diff line change
@@ -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
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
27 changes: 5 additions & 22 deletions Src/js/xsd-kotlin-gen/generate.js
Original file line number Diff line number Diff line change
@@ -99,13 +99,13 @@ function getType(rawType) {
"xs:anySimpleType": "String",
"xs:boolean": "Boolean",
"xs:integer": "Int",
"xs:decimal": "java.math.BigDecimal",
"xs:decimal": "@kotlinx.serialization.Contextual java.math.BigDecimal",
"xs:dateTime": "String",
"xs:time": "String",
"xs:date": "String",
"xs:base64Binary": "String",
"xs:anyURI": "String",
"xs:QName": 'nl.adaptivity.xmlutil.SerializableQName', // "javax.xml.namespace.QName", // "String",
"xs:QName": "@kotlinx.serialization.Serializable(org.hl7.elm_modelinfo.r1.serializing.QNameJsonSerializer::class) nl.adaptivity.xmlutil.QName",
"xs:token": "String",
"xs:NCName": "String",
"xs:ID": "String",
@@ -125,14 +125,6 @@ if ([
return name;
}

function addContextualAnnotationIfNecessary(type) {
if (type === 'java.math.BigDecimal') {
return `@kotlinx.serialization.Serializable(org.hl7.elm_modelinfo.r1.serializing.BigDecimalSerializer::class)`;
}

return ''
}

function parse(filePath) {
const xml = fs
.readFileSync(filePath, "utf8")
@@ -242,13 +234,8 @@ import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
import kotlinx.serialization.modules.contextual
object Serializer {
val serializersModule = kotlinx.serialization.modules.SerializersModule {
fun createSerializer(): kotlinx.serialization.modules.SerializersModule {
return kotlinx.serialization.modules.SerializersModule {
// contextual(org.cql.QNameSerializerForJson)
${[...getAllParentClasses(config)].reverse().map((parentClass) => {
@@ -263,10 +250,6 @@ object Serializer {
}).join('\n')}
}
}
}
`
@@ -641,7 +624,7 @@ ${attributesFields
// type === 'nl.adaptivity.xmlutil.SerializableQName' ? '@kotlinx.serialization.Contextual' : '@kotlinx.serialization.Serializable'
''
}
var ${makeFieldName(field.attributes.name)}: ${addContextualAnnotationIfNecessary(type)} ${type}? = null
var ${makeFieldName(field.attributes.name)}: ${type}? = null
get() {
return field ?: ${defaultValue}
}
@@ -657,7 +640,7 @@ ${attributesFields
// type === 'nl.adaptivity.xmlutil.SerializableQName' ? '@kotlinx.serialization.Contextual' : '@kotlinx.serialization.Serializable'
''
}
var ${makeFieldName(field.attributes.name)}: ${addContextualAnnotationIfNecessary(type)} ${type}? = null
var ${makeFieldName(field.attributes.name)}: ${type}? = null
${extraForBoolean}
${extraForWith}

0 comments on commit 4479b47

Please sign in to comment.