Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parsing and serializing CQL annotations in ELM XML and JSON (Kotlin feature branch) #1493

Merged
merged 2 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions Src/java/cql-to-elm/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ dependencies {

implementation("org.jetbrains.kotlinx:kotlinx-io-core-jvm:0.6.0")

// Temporary until we can get rid of the dependency on wrapping
// the CQL annotations in a JAXBElement for narrative generation
implementation("jakarta.xml.bind:jakarta.xml.bind-api:4.0.1")

testImplementation(project(":elm-xmlutil"))
testImplementation(project(":model-xmlutil"))
testImplementation(project(":quick"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@

package org.cqframework.cql.cql2elm.preprocessor

import jakarta.xml.bind.JAXBElement
import java.io.Serializable
import java.util.*
import javax.xml.namespace.QName
import org.antlr.v4.kotlinruntime.ParserRuleContext
import org.antlr.v4.kotlinruntime.TokenStream
import org.antlr.v4.kotlinruntime.misc.Interval
Expand Down Expand Up @@ -763,32 +760,32 @@ abstract class CqlPreprocessorElmCommonVisitor(
return Pair.of(header.substring(startFrom).trim { it <= ' ' }, header.length)
}

fun wrapNarrative(narrative: Narrative): Serializable {
fun wrapNarrative(narrative: Narrative): Any {
@Suppress("ForbiddenComment")
/*
TODO: Should be able to collapse narrative if the span doesn't have an attribute
That's what this code is doing, but it doesn't work and I don't have time to debug it
if (narrative.getR() == null) {
StringBuilder content = new StringBuilder();
boolean onlyStrings = true;
for (Serializable s : narrative.getContent()) {
if (s instanceof String) {
content.append((String)s);
This code collapses the narrative if the span doesn't have an attribute.
It does work but creates a different (simplified) ELM.

if (narrative.r == null) {
val content = StringBuilder()
var onlyStrings = true
for (s in narrative.content) {
if (s is String) {
content.append(s)
}
else {
onlyStrings = false;
onlyStrings = false
}
}
if (onlyStrings) {
return content.toString();
return content.toString()
}
}

return narrative
*/
return JAXBElement(
QName("urn:hl7-org:cql-annotations:r1", "s"),
Narrative::class.java,
narrative
)

return narrative
}

fun isValidIdentifier(tagName: String): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

import jakarta.xml.bind.JAXBElement;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
Expand Down Expand Up @@ -42,11 +41,10 @@ void comments() throws IOException {
Annotation a = (Annotation) def.getAnnotation().get(0);
assertThat(a.getS().getContent(), notNullValue());
assertThat(a.getS().getContent().size(), is(2));
assertThat(a.getS().getContent().get(0), instanceOf(JAXBElement.class));
JAXBElement e = (JAXBElement) a.getS().getContent().get(0);
var e = a.getS().getContent().get(0);
assertThat(e, notNullValue());
assertThat(e.getValue(), instanceOf(Narrative.class));
Narrative n = (Narrative) e.getValue();
assertThat(e, instanceOf(Narrative.class));
Narrative n = (Narrative) e;
assertThat(n.getContent(), notNullValue());
assertThat(n.getContent().size(), is(4));
assertThat(n.getContent().get(0), instanceOf(String.class));
Expand All @@ -63,11 +61,10 @@ void comments() throws IOException {
a = (Annotation) def.getAnnotation().get(0);
assertThat(a.getS().getContent(), notNullValue());
assertThat(a.getS().getContent().size(), is(2));
assertThat(a.getS().getContent().get(0), instanceOf(JAXBElement.class));
e = (JAXBElement) a.getS().getContent().get(0);
e = a.getS().getContent().get(0);
assertThat(e, notNullValue());
assertThat(e.getValue(), instanceOf(Narrative.class));
n = (Narrative) e.getValue();
assertThat(e, instanceOf(Narrative.class));
n = (Narrative) e;
assertThat(n.getContent(), notNullValue());
assertThat(n.getContent().size(), is(4));
assertThat(n.getContent().get(0), instanceOf(String.class));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package org.cqframework.cql.elm.requirements.fhir;

import ca.uhn.fhir.context.FhirVersionEnum;
import jakarta.xml.bind.JAXBElement;
import java.io.Serializable;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.List;
Expand Down Expand Up @@ -450,19 +448,12 @@ private String toNarrativeText(org.hl7.cql_annotations.r1.Annotation a) {
}

private void addNarrativeText(StringBuilder sb, org.hl7.cql_annotations.r1.Narrative n) {
for (Serializable s : n.getContent()) {
for (var s : n.getContent()) {
if (s instanceof org.hl7.cql_annotations.r1.Narrative) {
addNarrativeText(sb, (org.hl7.cql_annotations.r1.Narrative) s);
} else if (s instanceof String) {
sb.append((String) s);
}
// TODO: THIS IS WRONG... SHOULDN'T NEED TO KNOW ABOUT JAXB TO ACCOMPLISH THIS
else if (s instanceof JAXBElement<?>) {
JAXBElement<?> j = (JAXBElement<?>) s;
if (j.getValue() instanceof org.hl7.cql_annotations.r1.Narrative) {
addNarrativeText(sb, (org.hl7.cql_annotations.r1.Narrative) j.getValue());
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
import org.cqframework.cql.cql2elm.CqlCompilerOptions;
import org.cqframework.cql.cql2elm.CqlTranslator;
import org.cqframework.cql.cql2elm.LibraryBuilder;
import org.hl7.cql_annotations.r1.Annotation;
import org.hl7.cql_annotations.r1.CqlToElmInfo;
import org.hl7.cql_annotations.r1.Narrative;
import org.hl7.elm.r1.*;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

class ElmDeserializeTests {
Expand All @@ -30,7 +31,6 @@ void elmTests() {
}

@Test
@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 All @@ -55,8 +55,19 @@ void jsonANCFHIRDummyLibraryLoad() {
assertTrue(
((SingletonFrom) library.getStatements().getDef().get(0).getExpression()).getOperand()
instanceof Retrieve);
assertNotNull(library.getStatements().getDef().get(1));
assertTrue(library.getStatements().getDef().get(1).getExpression() instanceof Retrieve);
var observationsStatement = library.getStatements().getDef().get(1);
assertNotNull(observationsStatement);
assertTrue(observationsStatement.getExpression() instanceof Retrieve);

assertTrue(observationsStatement.getAnnotation().get(0) instanceof Annotation);
var annotation = (Annotation) observationsStatement.getAnnotation().get(0);
assertNotNull(annotation.getS());
var narrative = annotation.getS();
assertTrue(narrative.getContent().get(1) instanceof Narrative);
var nestedNarrative = (Narrative) narrative.getContent().get(1);
assertTrue(nestedNarrative.getContent().get(0) instanceof Narrative);
nestedNarrative = (Narrative) nestedNarrative.getContent().get(0);
assertEquals("[", nestedNarrative.getContent().get(0));

verifySigLevels(library, LibraryBuilder.SignatureLevel.All);
} catch (IOException e) {
Expand Down Expand Up @@ -100,7 +111,6 @@ void jsonAdultOutpatientEncountersFHIR4LibraryLoad() {
}

@Test
@Disabled("Invalid XML value at position: 85:29: Index -1 out of bounds for length 2")
void xmlLibraryLoad() {
try {
final Library library =
Expand Down Expand Up @@ -130,11 +140,20 @@ void xmlLibraryLoad() {
assertTrue(
((SingletonFrom) library.getStatements().getDef().get(0).getExpression()).getOperand()
instanceof Retrieve);
assertEquals(
"Qualifying Encounters",
library.getStatements().getDef().get(1).getName());
assertNotNull(library.getStatements().getDef().get(1));
assertTrue(library.getStatements().getDef().get(1).getExpression() instanceof Query);
var qualifyingEncountersStatement = library.getStatements().getDef().get(1);
assertEquals("Qualifying Encounters", qualifyingEncountersStatement.getName());
assertNotNull(qualifyingEncountersStatement);
assertTrue(qualifyingEncountersStatement.getExpression() instanceof Query);
assertTrue(qualifyingEncountersStatement.getAnnotation().get(0) instanceof Annotation);
var annotation =
(Annotation) qualifyingEncountersStatement.getAnnotation().get(0);
assertNotNull(annotation.getS());
var narrative = annotation.getS();
assertEquals("\n ", narrative.getContent().get(0));
assertTrue(narrative.getContent().get(3) instanceof Narrative);
var nestedNarrative = (Narrative) narrative.getContent().get(3);
assertEquals("\n ", nestedNarrative.getContent().get(0));
assertTrue(nestedNarrative.getContent().get(1) instanceof Narrative);

verifySigLevels(library, LibraryBuilder.SignatureLevel.Overloads);
} catch (IOException e) {
Expand All @@ -144,7 +163,6 @@ void xmlLibraryLoad() {
}

@Test
@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 @@ -200,7 +218,6 @@ private void testElmDeserialization(String directoryName) throws URISyntaxExcept
}

@Test
@Disabled("Invalid XML value at position: 59:29: Index -1 out of bounds for length 2")
void regressionTestJsonSerializer() throws URISyntaxException {
// This test validates that the ELM library deserialized from the Json matches the ELM library deserialized from
// Xml
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package org.cqframework.cql.elm.serializing.xmlutil
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.plus
import kotlinx.serialization.modules.serializersModuleOf
import org.cqframework.cql.elm.serializing.NarrativeJsonSerializer
import org.hl7.elm_modelinfo.r1.serializing.BigDecimalJsonSerializer

val json = Json {
serializersModule =
serializersModuleOf(BigDecimalJsonSerializer) +
serializersModuleOf(NarrativeJsonSerializer) +
org.hl7.elm.r1.serializersModule +
org.hl7.cql_annotations.r1.serializersModule
explicitNulls = false
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,61 @@
package org.cqframework.cql.elm.serializing.xmlutil

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.plus
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.serializersModuleOf
import nl.adaptivity.xmlutil.QName
import nl.adaptivity.xmlutil.XMLConstants
import nl.adaptivity.xmlutil.serialization.DefaultXmlSerializationPolicy
import nl.adaptivity.xmlutil.serialization.XML
import nl.adaptivity.xmlutil.serialization.structure.SafeParentInfo
import org.hl7.elm_modelinfo.r1.serializing.BigDecimalXmlSerializer

val builder =
DefaultXmlSerializationPolicy.Builder().apply {
// Use xsi:type for handling polymorphism
typeDiscriminatorName = QName(XMLConstants.XSI_NS_URI, "type", XMLConstants.XSI_PREFIX)
}

@OptIn(ExperimentalSerializationApi::class)
val customPolicy =
object : DefaultXmlSerializationPolicy(builder) {
override fun isTransparentPolymorphic(
serializerParent: SafeParentInfo,
tagParent: SafeParentInfo
): Boolean {
// Switch on transparent polymorphic mode for mixed content
if (
serializerParent.elementSerialDescriptor.serialName ==
"kotlinx.serialization.Polymorphic<Any>"
) {
return true
}
return super.isTransparentPolymorphic(serializerParent, tagParent)
}
}

// Mixed content can include text and Narrative elements
val mixedContentSerializersModule = SerializersModule {
polymorphic(Any::class) {
polymorphic(Any::class, String::class, String.serializer())
polymorphic(
Any::class,
org.hl7.cql_annotations.r1.Narrative::class,
org.hl7.cql_annotations.r1.Narrative.serializer()
)
}
}

val xml =
XML(
serializersModuleOf(BigDecimalXmlSerializer) +
mixedContentSerializersModule +
org.hl7.elm.r1.serializersModule +
org.hl7.cql_annotations.r1.serializersModule
) {
policy = customPolicy
xmlDeclMode = nl.adaptivity.xmlutil.XmlDeclMode.Charset
defaultPolicy {
typeDiscriminatorName =
QName("http://www.w3.org/2001/XMLSchema-instance", "type", "xsi")
}
}
Loading
Loading