diff --git a/org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/test/FHIRMappingLanguageTests.java b/org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/test/FHIRMappingLanguageTests.java index 869c89008..303be9aaa 100644 --- a/org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/test/FHIRMappingLanguageTests.java +++ b/org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/test/FHIRMappingLanguageTests.java @@ -80,9 +80,8 @@ public class FHIRMappingLanguageTests implements ITransformerServices { @BeforeClass static public void setUp() throws Exception { if (context == null) { - context = new SimpleWorkerContext(); PackageCacheManager pcm = new PackageCacheManager(true, ToolsVersion.TOOLS_VERSION); - context.loadFromPackage(pcm.loadPackageCacheLatest("hl7.fhir.core"), null, "StructureDefinition"); + context = SimpleWorkerContext.fromPackage(pcm.loadPackage("hl7.fhir.core", "4.0.0")); jsonParser = new JsonParser(); jsonParser.setOutputStyle(OutputStyle.PRETTY); } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/StructureMapUtilities.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/StructureMapUtilities.java index 9c047ea71..de8895ce3 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/StructureMapUtilities.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/StructureMapUtilities.java @@ -1170,11 +1170,13 @@ public class StructureMapUtilities { target.addListMode(StructureMapTargetListMode.SHARE); lexer.next(); target.setListRuleId(lexer.take()); - } else if (lexer.getCurrent().equals("first")) - target.addListMode(StructureMapTargetListMode.FIRST); - else - target.addListMode(StructureMapTargetListMode.LAST); - lexer.next(); + } else { + if (lexer.getCurrent().equals("first")) + target.addListMode(StructureMapTargetListMode.FIRST); + else + target.addListMode(StructureMapTargetListMode.LAST); + lexer.next(); + } } } @@ -1219,7 +1221,7 @@ public class StructureMapUtilities { } public enum VariableMode { - INPUT, OUTPUT + INPUT, OUTPUT, SHARED } public class Variable { @@ -1277,16 +1279,25 @@ public class StructureMapUtilities { return null; } - public String summary() { - CommaSeparatedStringBuilder s = new CommaSeparatedStringBuilder(); - CommaSeparatedStringBuilder t = new CommaSeparatedStringBuilder(); - for (Variable v : list) - if (v.mode == VariableMode.INPUT) - s.append(v.summary()); - else - t.append(v.summary()); - return "source variables ["+s.toString()+"], target variables ["+t.toString()+"]"; - } + public String summary() { + CommaSeparatedStringBuilder s = new CommaSeparatedStringBuilder(); + CommaSeparatedStringBuilder t = new CommaSeparatedStringBuilder(); + CommaSeparatedStringBuilder sh = new CommaSeparatedStringBuilder(); + for (Variable v : list) + switch(v.mode) { + case INPUT: + s.append(v.summary()); + break; + case OUTPUT: + t.append(v.summary()); + break; + case SHARED: + sh.append(v.summary()); + break; + } + return "source variables ["+s.toString()+"], target variables ["+t.toString()+"], shared variables ["+sh.toString()+"]"; + } + } public class TransformContext { @@ -1379,7 +1390,7 @@ public class StructureMapUtilities { if (source != null) { for (Variables v : source) { for (StructureMapGroupRuleTargetComponent t : rule.getTarget()) { - processTarget(rule.getName(), context, v, map, group, t, rule.getSource().size() == 1 ? rule.getSourceFirstRep().getVariable() : null, atRoot); + processTarget(rule.getName(), context, v, map, group, t, rule.getSource().size() == 1 ? rule.getSourceFirstRep().getVariable() : null, atRoot, vars); } if (rule.hasRule()) { for (StructureMapGroupRuleComponent childrule : rule.getRule()) { @@ -1781,7 +1792,7 @@ public class StructureMapUtilities { return false; } - private void processTarget(String ruleId, TransformContext context, Variables vars, StructureMap map, StructureMapGroupComponent group, StructureMapGroupRuleTargetComponent tgt, String srcVar, boolean atRoot) throws FHIRException { + private void processTarget(String ruleId, TransformContext context, Variables vars, StructureMap map, StructureMapGroupComponent group, StructureMapGroupRuleTargetComponent tgt, String srcVar, boolean atRoot, Variables sharedVars) throws FHIRException { Base dest = null; if (tgt.hasContext()) { dest = vars.get(VariableMode.OUTPUT, tgt.getContext()); @@ -1795,8 +1806,17 @@ public class StructureMapUtilities { v = runTransform(ruleId, context, map, group, tgt, vars, dest, tgt.getElement(), srcVar, atRoot); if (v != null && dest != null) v = dest.setProperty(tgt.getElement().hashCode(), tgt.getElement(), v); // reset v because some implementations may have to rewrite v when setting the value - } else if (dest != null) - v = dest.makeProperty(tgt.getElement().hashCode(), tgt.getElement()); + } else if (dest != null) { + if (tgt.hasListMode(StructureMapTargetListMode.SHARE)) { + v = sharedVars.get(VariableMode.SHARED, tgt.getListRuleId()); + if (v == null) { + v = dest.makeProperty(tgt.getElement().hashCode(), tgt.getElement()); + sharedVars.add(VariableMode.SHARED, tgt.getListRuleId(), v); + } + } else { + v = dest.makeProperty(tgt.getElement().hashCode(), tgt.getElement()); + } + } if (tgt.hasVariable() && v != null) vars.add(VariableMode.OUTPUT, tgt.getVariable(), v); } diff --git a/org.hl7.fhir.r5/src/main/resources/fml/.gitignore b/org.hl7.fhir.r5/src/main/resources/fml/.gitignore new file mode 100644 index 000000000..fa929750c --- /dev/null +++ b/org.hl7.fhir.r5/src/main/resources/fml/.gitignore @@ -0,0 +1 @@ +*.out \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/main/resources/fml/manifest.xml b/org.hl7.fhir.r5/src/main/resources/fml/manifest.xml new file mode 100644 index 000000000..89b3d6b31 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/resources/fml/manifest.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/main/resources/fml/qr.json b/org.hl7.fhir.r5/src/main/resources/fml/qr.json new file mode 100644 index 000000000..f0d14a3c1 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/resources/fml/qr.json @@ -0,0 +1,39 @@ +{ + "resourceType": "QuestionnaireResponse", + "status": "in-progress", + "item": [ + { + "linkId": "patient", + "text": "Patient", + "item": [ + { + "linkId": "patient.lastname", + "text": "Name", + "answer": [ + { + "valueString": "Brönnimann-Bertholet" + } + ] + }, + { + "linkId": "patient.firstname", + "text": "Vorname", + "answer": [ + { + "valueString": "Elisabeth" + } + ] + }, + { + "linkId": "patient.sex", + "text": "Geschlecht", + "answer": [ + { + "valueString": "female" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-assignment-res.json b/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-assignment-res.json new file mode 100644 index 000000000..c29553577 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-assignment-res.json @@ -0,0 +1,4 @@ +{ + "resourceType" : "Patient", + "gender" : "female" +} \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-assignment.map b/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-assignment.map new file mode 100644 index 000000000..79ddfb89b --- /dev/null +++ b/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-assignment.map @@ -0,0 +1,8 @@ +map "http://github.com/hapifhir/org.hl7.fhir.core/org.hl7.fhir.r4.tests/qr2patassignment" = "qr2patassignment" + +uses "http://hl7.org/fhir/StructureDefinition/QuestionnaireResponse" alias QuestionnaireResponse as source +uses "http://hl7.org/fhir/StructureDefinition/Patient" alias Patient as target + +group QuestionnaireResponse(source src : QuestionnaireResponse, target tgt : Patient) { + src -> tgt.gender = 'female' "Simple Assignment"; +} \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-gender-res.json b/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-gender-res.json new file mode 100644 index 000000000..c29553577 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-gender-res.json @@ -0,0 +1,4 @@ +{ + "resourceType" : "Patient", + "gender" : "female" +} \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-gender.map b/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-gender.map new file mode 100644 index 000000000..ee25265c6 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-gender.map @@ -0,0 +1,12 @@ +map "http://github.com/hapifhir/org.hl7.fhir.core/org.hl7.fhir.r4.tests/qr2patgender" = "qr2patgender" + +uses "http://hl7.org/fhir/StructureDefinition/QuestionnaireResponse" alias QuestionnaireResponse as source +uses "http://hl7.org/fhir/StructureDefinition/Patient" alias Patient as target + +group QuestionnaireResponse(source src : QuestionnaireResponse, target tgt : Patient) { + src.item as item -> tgt as patient then item(item, patient); +} + +group item(source src, target tgt: Patient) { + src.item as item where linkId.value in ('patient.sex') -> tgt.gender = (item.answer.valueString); +} diff --git a/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-humannameshared-res.json b/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-humannameshared-res.json new file mode 100644 index 000000000..240a91c69 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-humannameshared-res.json @@ -0,0 +1,8 @@ +{ + "resourceType" : "Patient", + "name" : [{ + "family" : "Brönnimann-Bertholet", + "given" : ["Elisabeth"] + }], + "gender" : "female" +} diff --git a/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-humannameshared.map b/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-humannameshared.map new file mode 100644 index 000000000..178a5d658 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-humannameshared.map @@ -0,0 +1,26 @@ +map "http://github.com/hapifhir/org.hl7.fhir.core/org.hl7.fhir.r4.tests/qr2pathumannameshared" = "qr2pathumannametwice" + +uses "http://hl7.org/fhir/StructureDefinition/QuestionnaireResponse" alias QuestionnaireResponse as source +uses "http://hl7.org/fhir/StructureDefinition/Patient" alias Patient as target + +group entry(source src : QuestionnaireResponse, target tgt : Patient) { + src.item as item then item(item, tgt); +} + +group item(source src, target tgt) { + src.item as item then item(item, tgt); + src.item as item where linkId.value = 'patient.lastname' -> tgt.name as name share patientName then humanNameFamily(item, name); + src.item as item where linkId.value = 'patient.firstname' -> tgt.name as name share patientName then humanNameGiven(item, name); + src.item as item where linkId.value = 'patient.sex' -> tgt.gender = (item.answer.valueString); +} + +group humanNameFamily(source src, target tgt: HumanName) { + src.answer as answer -> tgt.family = (answer.valueString); +} +group humanNameGiven(source src, target tgt: HumanName) { + src.answer as answer -> tgt.given = (answer.valueString); +} + +group administrativeGender(source src, target tgt: code) { + src.answer as answer -> tgt = (answer.valueString); +} diff --git a/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-humannametwice-res.json b/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-humannametwice-res.json new file mode 100644 index 000000000..9d9bab7db --- /dev/null +++ b/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-humannametwice-res.json @@ -0,0 +1,10 @@ +{ + "resourceType" : "Patient", + "name" : [{ + "family" : "Brönnimann-Bertholet" + }, + { + "given" : ["Elisabeth"] + }], + "gender" : "female" +} \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-humannametwice.map b/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-humannametwice.map new file mode 100644 index 000000000..6de8b3c88 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/resources/fml/qr2pat-humannametwice.map @@ -0,0 +1,26 @@ +map "http://github.com/hapifhir/org.hl7.fhir.core/org.hl7.fhir.r4.tests/qr2pathumannametwice" = "qr2pathumannametwice" + +uses "http://hl7.org/fhir/StructureDefinition/QuestionnaireResponse" alias QuestionnaireResponse as source +uses "http://hl7.org/fhir/StructureDefinition/Patient" alias Patient as target + +group entry(source src : QuestionnaireResponse, target tgt : Patient) { + src.item as item then item(item, tgt); +} + +group item(source src, target tgt) { + src.item as item then item(item, tgt); + src.item as item where linkId.value = 'patient.lastname' -> tgt.name as name then humanNameFamily(item, name); + src.item as item where linkId.value = 'patient.firstname' -> tgt.name as name then humanNameGiven(item, name); + src.item as item where linkId.value = 'patient.sex' -> tgt.gender = (item.answer.valueString); +} + +group humanNameFamily(source src, target tgt: HumanName) { + src.answer as answer -> tgt.family = (answer.valueString); +} +group humanNameGiven(source src, target tgt: HumanName) { + src.answer as answer -> tgt.given = (answer.valueString); +} + +group administrativeGender(source src, target tgt: code) { + src.answer as answer -> tgt = (answer.valueString); +} diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/FHIRMappingLanguageTests.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/FHIRMappingLanguageTests.java new file mode 100644 index 000000000..906aba456 --- /dev/null +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/FHIRMappingLanguageTests.java @@ -0,0 +1,167 @@ +package org.hl7.fhir.r5.test; + +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.parsers.ParserConfigurationException; + +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.r5.context.SimpleWorkerContext; +import org.hl7.fhir.r5.elementmodel.Manager; +import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat; +import org.hl7.fhir.r5.formats.IParser.OutputStyle; +import org.hl7.fhir.r5.formats.JsonParser; +import org.hl7.fhir.r5.model.Base; +import org.hl7.fhir.r5.model.Coding; +import org.hl7.fhir.r5.model.Resource; +import org.hl7.fhir.r5.model.ResourceFactory; +import org.hl7.fhir.r5.model.StructureDefinition; +import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind; +import org.hl7.fhir.r5.model.StructureMap; +import org.hl7.fhir.r5.terminologies.ConceptMapEngine; +import org.hl7.fhir.r5.test.utils.TestingUtilities; +import org.hl7.fhir.r5.utils.StructureMapUtilities; +import org.hl7.fhir.r5.utils.StructureMapUtilities.ITransformerServices; +import org.hl7.fhir.utilities.TextFile; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.cache.PackageCacheManager; +import org.hl7.fhir.utilities.cache.ToolsVersion; +import org.hl7.fhir.utilities.xml.XMLUtil; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +@RunWith(Parameterized.class) + +public class FHIRMappingLanguageTests implements ITransformerServices { + + private List outputs = new ArrayList(); + + static private SimpleWorkerContext context; + static private JsonParser jsonParser; + + @Parameters(name = "{index}: {0}") + public static Iterable data() + throws FileNotFoundException, IOException, ParserConfigurationException, SAXException { + Document tests = XMLUtil.parseFileToDom(TestingUtilities.resourceNameToFile("fml", "manifest.xml")); + Element test = XMLUtil.getFirstChild(tests.getDocumentElement()); + List objects = new ArrayList(); + while (test != null && test.getNodeName().equals("test")) { + objects.add(new Object[] { test.getAttribute("name"), test.getAttribute("source"), test.getAttribute("map"), + test.getAttribute("output") }); + test = XMLUtil.getNextSibling(test); + } + return objects; + } + + private final String name; + private String source; + private String output; + private String map; + + public FHIRMappingLanguageTests(String name, String source, String map, String output) { + this.name = name; + this.source = source; + this.output = output; + this.map = map; + } + + @BeforeClass + static public void setUp() throws Exception { + if (context == null) { + PackageCacheManager pcm = new PackageCacheManager(true, ToolsVersion.TOOLS_VERSION); + context = SimpleWorkerContext.fromPackage(pcm.loadPackage("hl7.fhir.core", "4.0.0")); + jsonParser = new JsonParser(); + jsonParser.setOutputStyle(OutputStyle.PRETTY); + } + } + + @Test + public void test() throws Exception { + + String fileSource = TestingUtilities.resourceNameToFile("fml", source); + String fileMap = TestingUtilities.resourceNameToFile("fml", map); + String fileOutput = TestingUtilities.resourceNameToFile("fml", output); + String fileOutputRes = TestingUtilities.resourceNameToFile("fml", output)+".out"; + + outputs.clear(); + + boolean ok = false; + String msg = null; + Resource resource = null; + try { + StructureMapUtilities scu = new StructureMapUtilities(context, this); + org.hl7.fhir.r5.elementmodel.Element src = Manager.parse(context, + new ByteArrayInputStream(TextFile.fileToBytes(fileSource)), FhirFormat.JSON); + StructureMap structureMap = scu.parse(TextFile.fileToString(fileMap), name); + String typeName = scu.getTargetType(structureMap).getType(); + resource = ResourceFactory.createResource(typeName); + scu.transform(null, src, structureMap, resource); + ok = true; + } catch (Exception e) { + ok = false; + msg = e.getMessage(); + } + if (ok) { + ByteArrayOutputStream boas = new ByteArrayOutputStream(); + jsonParser.compose(boas, resource); + log(boas.toString()); + TextFile.bytesToFile(boas.toByteArray(), fileOutputRes); + msg = TestingUtilities.checkJsonIsSame(fileOutputRes,fileOutput); + assertTrue(msg, Utilities.noString(msg)); + } else + assertTrue("Error, but proper output was expected (" + msg + ")", output.equals("$error")); + } + + @Override + public void log(String message) { + System.out.println(message); + } + + @Override + public Base createType(Object appInfo, String name) throws FHIRException { + StructureDefinition sd = context.fetchResource(StructureDefinition.class, name); + if (sd != null && sd.getKind() == StructureDefinitionKind.LOGICAL) { + return Manager.build(context, sd); + } else { + if (name.startsWith("http://hl7.org/fhir/StructureDefinition/")) + name = name.substring("http://hl7.org/fhir/StructureDefinition/".length()); + return ResourceFactory.createResourceOrType(name); + } + } + + @Override + public Base createResource(Object appInfo, Base res, boolean atRootofTransform) { + if (atRootofTransform) + outputs.add((Resource) res); + return res; + } + + @Override + public Coding translate(Object appInfo, Coding source, String conceptMapUrl) throws FHIRException { + ConceptMapEngine cme = new ConceptMapEngine(context); + return cme.translate(source, conceptMapUrl); + } + + @Override + public Base resolveReference(Object appContext, String url) throws FHIRException { + throw new FHIRException("resolveReference is not supported yet"); + } + + @Override + public List performSearch(Object appContext, String url) throws FHIRException { + throw new FHIRException("performSearch is not supported yet"); + } + +}