diff --git a/Src/java/elm-test/src/test/java/org/cqframework/cql/elm/ElmDeserializeTests.java b/Src/java/elm-test/src/test/java/org/cqframework/cql/elm/ElmDeserializeTests.java index dace5f414..dda11ac09 100644 --- a/Src/java/elm-test/src/test/java/org/cqframework/cql/elm/ElmDeserializeTests.java +++ b/Src/java/elm-test/src/test/java/org/cqframework/cql/elm/ElmDeserializeTests.java @@ -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 { diff --git a/Src/java/elm-xmlutil/src/main/java/org/cqframework/cql/elm/serializing/xmlutil/ElmJsonLibraryCommon.kt b/Src/java/elm-xmlutil/src/main/java/org/cqframework/cql/elm/serializing/xmlutil/ElmJsonLibraryCommon.kt new file mode 100644 index 000000000..273d86f2c --- /dev/null +++ b/Src/java/elm-xmlutil/src/main/java/org/cqframework/cql/elm/serializing/xmlutil/ElmJsonLibraryCommon.kt @@ -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 +} diff --git a/Src/java/elm-xmlutil/src/main/java/org/cqframework/cql/elm/serializing/xmlutil/ElmJsonLibraryReader.kt b/Src/java/elm-xmlutil/src/main/java/org/cqframework/cql/elm/serializing/xmlutil/ElmJsonLibraryReader.kt index 43e99b76f..0c9bb5f9f 100644 --- a/Src/java/elm-xmlutil/src/main/java/org/cqframework/cql/elm/serializing/xmlutil/ElmJsonLibraryReader.kt +++ b/Src/java/elm-xmlutil/src/main/java/org/cqframework/cql/elm/serializing/xmlutil/ElmJsonLibraryReader.kt @@ -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) diff --git a/Src/java/elm-xmlutil/src/main/java/org/cqframework/cql/elm/serializing/xmlutil/ElmJsonLibraryWriter.kt b/Src/java/elm-xmlutil/src/main/java/org/cqframework/cql/elm/serializing/xmlutil/ElmJsonLibraryWriter.kt index e51e0aa81..4e187894a 100644 --- a/Src/java/elm-xmlutil/src/main/java/org/cqframework/cql/elm/serializing/xmlutil/ElmJsonLibraryWriter.kt +++ b/Src/java/elm-xmlutil/src/main/java/org/cqframework/cql/elm/serializing/xmlutil/ElmJsonLibraryWriter.kt @@ -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)) } } diff --git a/Src/java/elm-xmlutil/src/main/java/org/cqframework/cql/elm/serializing/xmlutil/ElmXmlLibraryCommon.kt b/Src/java/elm-xmlutil/src/main/java/org/cqframework/cql/elm/serializing/xmlutil/ElmXmlLibraryCommon.kt new file mode 100644 index 000000000..df2ea67b3 --- /dev/null +++ b/Src/java/elm-xmlutil/src/main/java/org/cqframework/cql/elm/serializing/xmlutil/ElmXmlLibraryCommon.kt @@ -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") + } + } diff --git a/Src/java/elm-xmlutil/src/main/java/org/cqframework/cql/elm/serializing/xmlutil/ElmXmlLibraryReader.kt b/Src/java/elm-xmlutil/src/main/java/org/cqframework/cql/elm/serializing/xmlutil/ElmXmlLibraryReader.kt index 4de620d25..4fd09e1b8 100644 --- a/Src/java/elm-xmlutil/src/main/java/org/cqframework/cql/elm/serializing/xmlutil/ElmXmlLibraryReader.kt +++ b/Src/java/elm-xmlutil/src/main/java/org/cqframework/cql/elm/serializing/xmlutil/ElmXmlLibraryReader.kt @@ -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)) } } diff --git a/Src/java/elm-xmlutil/src/main/java/org/cqframework/cql/elm/serializing/xmlutil/ElmXmlLibraryWriter.kt b/Src/java/elm-xmlutil/src/main/java/org/cqframework/cql/elm/serializing/xmlutil/ElmXmlLibraryWriter.kt index 770abb9a3..30407cb72 100644 --- a/Src/java/elm-xmlutil/src/main/java/org/cqframework/cql/elm/serializing/xmlutil/ElmXmlLibraryWriter.kt +++ b/Src/java/elm-xmlutil/src/main/java/org/cqframework/cql/elm/serializing/xmlutil/ElmXmlLibraryWriter.kt @@ -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) } diff --git a/Src/java/elm-xmlutil/src/test/java/org/cqframework/cql/elm/serializing/xmlutil/CMS146JsonTest.java b/Src/java/elm-xmlutil/src/test/java/org/cqframework/cql/elm/serializing/xmlutil/CMS146JsonTest.java index e5f86f974..1ea70f86c 100644 --- a/Src/java/elm-xmlutil/src/test/java/org/cqframework/cql/elm/serializing/xmlutil/CMS146JsonTest.java +++ b/Src/java/elm-xmlutil/src/test/java/org/cqframework/cql/elm/serializing/xmlutil/CMS146JsonTest.java @@ -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); } diff --git a/Src/java/elm-xmlutil/src/test/java/org/cqframework/cql/elm/serializing/xmlutil/CMS146XmlTest.java b/Src/java/elm-xmlutil/src/test/java/org/cqframework/cql/elm/serializing/xmlutil/CMS146XmlTest.java index 275d35d64..e694d0670 100644 --- a/Src/java/elm-xmlutil/src/test/java/org/cqframework/cql/elm/serializing/xmlutil/CMS146XmlTest.java +++ b/Src/java/elm-xmlutil/src/test/java/org/cqframework/cql/elm/serializing/xmlutil/CMS146XmlTest.java @@ -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\""); diff --git a/Src/java/elm-xmlutil/src/test/java/org/cqframework/cql/elm/serializing/xmlutil/ElmXmlutilTest.java b/Src/java/elm-xmlutil/src/test/java/org/cqframework/cql/elm/serializing/xmlutil/ElmXmlutilTest.java index 1cc067b00..c74978847 100644 --- a/Src/java/elm-xmlutil/src/test/java/org/cqframework/cql/elm/serializing/xmlutil/ElmXmlutilTest.java +++ b/Src/java/elm-xmlutil/src/test/java/org/cqframework/cql/elm/serializing/xmlutil/ElmXmlutilTest.java @@ -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( diff --git a/Src/java/model-xmlutil/src/main/java/org/hl7/elm_modelinfo/r1/serializing/xmlutil/XmlModelInfoReader.kt b/Src/java/model-xmlutil/src/main/java/org/hl7/elm_modelinfo/r1/serializing/xmlutil/XmlModelInfoReader.kt index c85579790..935f0a715 100644 --- a/Src/java/model-xmlutil/src/main/java/org/hl7/elm_modelinfo/r1/serializing/xmlutil/XmlModelInfoReader.kt +++ b/Src/java/model-xmlutil/src/main/java/org/hl7/elm_modelinfo/r1/serializing/xmlutil/XmlModelInfoReader.kt @@ -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(), diff --git a/Src/java/model/src/jvmMain/kotlin/org/hl7/elm_modelinfo/r1/serializing/BigDecimalSerializer.kt b/Src/java/model/src/jvmMain/kotlin/org/hl7/elm_modelinfo/r1/serializing/BigDecimalSerializer.kt index ab0065bde..9b9cf24c4 100644 --- a/Src/java/model/src/jvmMain/kotlin/org/hl7/elm_modelinfo/r1/serializing/BigDecimalSerializer.kt +++ b/Src/java/model/src/jvmMain/kotlin/org/hl7/elm_modelinfo/r1/serializing/BigDecimalSerializer.kt @@ -5,15 +5,29 @@ import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* -object BigDecimalSerializer : KSerializer { +object BigDecimalXmlSerializer : KSerializer { 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 { + 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() + } +} \ No newline at end of file diff --git a/Src/java/model/src/jvmMain/kotlin/org/hl7/elm_modelinfo/r1/serializing/QNameJsonSerializer.kt b/Src/java/model/src/jvmMain/kotlin/org/hl7/elm_modelinfo/r1/serializing/QNameJsonSerializer.kt new file mode 100644 index 000000000..d2b3e9074 --- /dev/null +++ b/Src/java/model/src/jvmMain/kotlin/org/hl7/elm_modelinfo/r1/serializing/QNameJsonSerializer.kt @@ -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 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()) + } +} + diff --git a/Src/js/xsd-kotlin-gen/generate.js b/Src/js/xsd-kotlin-gen/generate.js index d11602a8d..e0b777400 100644 --- a/Src/js/xsd-kotlin-gen/generate.js +++ b/Src/js/xsd-kotlin-gen/generate.js @@ -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}