From 66f0b35a38178284e4c029d4564e8e7aebab69fa Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Wed, 16 Sep 2020 13:08:02 +1000 Subject: [PATCH] FHIRPath - support lenient mode on polymorphics --- RELEASE_NOTES.md | 5 + .../org/hl7/fhir/r5/utils/FHIRPathEngine.java | 29 +++- .../org/hl7/fhir/r5/test/FHIRPathTests.java | 149 +++++++++++------- pom.xml | 2 +- 4 files changed, 125 insertions(+), 60 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e69de29bb..c8185536a 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -0,0 +1,5 @@ +Validator: +* No changes + +Other code changes: +* Support lenient mode on FIHRPath when referring to polymorphics diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/FHIRPathEngine.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/FHIRPathEngine.java index e7a7f33a2..609e4ffec 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/FHIRPathEngine.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/FHIRPathEngine.java @@ -105,6 +105,7 @@ import ca.uhn.fhir.util.ElementUtil; * */ public class FHIRPathEngine { + private enum Equality { Null, True, False } private class FHIRConstant extends Base { @@ -211,6 +212,7 @@ public class FHIRPathEngine { private ValidationOptions terminologyServiceOptions = new ValidationOptions(); private ProfileUtilities profileUtilities; private String location; // for error messages + private boolean allowPolymorphicNames; // if the fhir path expressions are allowed to use constants beyond those defined in the specification // the application can implement them by providing a constant resolver @@ -372,10 +374,24 @@ public class FHIRPathEngine { * @throws FHIRException */ protected void getChildrenByName(Base item, String name, List result) throws FHIRException { + String tn = null; + if (isAllowPolymorphicNames()) { + // we'll look to see whether we hav a polymorphic name + for (Property p : item.children()) { + if (p.getName().endsWith("[x]")) { + String n = p.getName().substring(0, p.getName().length()-3); + if (name.startsWith(n)) { + tn = name.substring(n.length()); + name = n; + break; + } + } + } + } Base[] list = item.listChildrenByName(name, false); if (list != null) { for (Base v : list) { - if (v != null) { + if (v != null && (tn == null || v.fhirType().equalsIgnoreCase(tn))) { result.add(v); } } @@ -5166,7 +5182,7 @@ public class FHIRPathEngine { } else { path = sdi.getSnapshot().getElement().get(0).getPath()+tail+"."+name; - ElementDefinitionMatch ed = getElementDefinition(sdi, path, false); + ElementDefinitionMatch ed = getElementDefinition(sdi, path, isAllowPolymorphicNames()); if (ed != null) { if (!Utilities.noString(ed.getFixedType())) result.addType(ed.getFixedType()); @@ -5556,5 +5572,14 @@ public class FHIRPathEngine { public IWorkerContext getWorker() { return worker; } + + public boolean isAllowPolymorphicNames() { + return allowPolymorphicNames; + } + + public void setAllowPolymorphicNames(boolean allowPolymorphicNames) { + this.allowPolymorphicNames = allowPolymorphicNames; + } + } \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/FHIRPathTests.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/FHIRPathTests.java index 3aac680b5..108ea465a 100644 --- a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/FHIRPathTests.java +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/FHIRPathTests.java @@ -24,6 +24,7 @@ import org.hl7.fhir.r5.model.Quantity; import org.hl7.fhir.r5.model.Resource; import org.hl7.fhir.r5.model.TypeDetails; import org.hl7.fhir.r5.model.ValueSet; +import org.hl7.fhir.r5.test.FHIRPathTests.TestResultType; import org.hl7.fhir.r5.test.utils.TestingUtilities; import org.hl7.fhir.r5.utils.FHIRPathEngine; import org.hl7.fhir.r5.utils.FHIRPathEngine.IEvaluationContext; @@ -41,6 +42,8 @@ import org.xml.sax.SAXException; public class FHIRPathTests { + public enum TestResultType {OK, SYNTAX, SEMANTICS, EXECUTION} + public class FHIRPathTestEvaluationServices implements IEvaluationContext { @Override @@ -111,6 +114,7 @@ public class FHIRPathTests { XMLUtil.getNamedChildren(dom.getDocumentElement(), "group", groups); for (Element g : groups) { XMLUtil.getNamedChildren(g, "test", list); + XMLUtil.getNamedChildren(g, "modeTest", list); } List objects = new ArrayList<>(); @@ -147,84 +151,115 @@ public class FHIRPathTests { public void test(String name, Element test) throws FileNotFoundException, IOException, FHIRException, org.hl7.fhir.exceptions.FHIRException, UcumException { // Setting timezone for this test. Grahame is in UTC+11, Travis is in GMT, and I'm here in Toronto, Canada with // all my time based tests failing locally... - TimeZone.setDefault(TimeZone.getTimeZone("UTC+1100")); + TimeZone.setDefault(TimeZone.getTimeZone("UTC+1100")); fp.setHostServices(new FHIRPathTestEvaluationServices()); String input = test.getAttribute("inputfile"); String expression = XMLUtil.getNamedChild(test, "expression").getTextContent(); - boolean fail = Utilities.existsInList(XMLUtil.getNamedChild(test, "expression").getAttribute("invalid"), "true", "semantic"); + TestResultType fail = TestResultType.OK; + if ("syntax".equals(XMLUtil.getNamedChild(test, "expression").getAttribute("invalid"))) { + fail = TestResultType.SYNTAX; + } else if ("semantic".equals(XMLUtil.getNamedChild(test, "expression").getAttribute("invalid"))) { + fail = TestResultType.SEMANTICS; + } else if ("execution".equals(XMLUtil.getNamedChild(test, "expression").getAttribute("invalid"))) { + fail = TestResultType.EXECUTION; + }; + fp.setAllowPolymorphicNames("lenient/polymorphics".equals(test.getAttribute("mode"))); Resource res = null; List outcome = new ArrayList(); - ExpressionNode node = fp.parse(expression); + System.out.println(name); + + ExpressionNode node = null; try { - if (Utilities.noString(input)) { - fp.check(null, null, node); - } else { - res = resources.get(input); - if (res == null) { - res = new XmlParser().parse(TestingUtilities.loadTestResourceStream("r5", input)); - resources.put(input, res); - } - fp.check(res, res.getResourceType().toString(), res.getResourceType().toString(), node); - } - outcome = fp.evaluate(res, node); - Assertions.assertFalse(fail, String.format("Expected exception parsing %s", expression)); + node = fp.parse(expression); + Assertions.assertTrue(fail != TestResultType.SYNTAX, String.format("Expected exception didn't occur parsing %s", expression)); } catch (Exception e) { - Assertions.assertTrue(fail, String.format("Unexpected exception parsing %s: " + e.getMessage(), expression)); + Assertions.assertTrue(fail == TestResultType.SYNTAX, String.format("Unexpected exception parsing %s: " + e.getMessage(), expression)); + } + + if (node != null) { + try { + if (Utilities.noString(input)) { + fp.check(null, null, node); + } else { + res = resources.get(input); + if (res == null) { + res = new XmlParser().parse(TestingUtilities.loadTestResourceStream("r5", input)); + resources.put(input, res); + } + fp.check(res, res.getResourceType().toString(), res.getResourceType().toString(), node); + } + Assertions.assertTrue(fail != TestResultType.SEMANTICS, String.format("Expected exception didn't occur checking %s", expression)); + } catch (Exception e) { + Assertions.assertTrue(fail == TestResultType.SEMANTICS, String.format("Unexpected exception checking %s: " + e.getMessage(), expression)); + node = null; + } + } + + if (node != null) { + try { + outcome = fp.evaluate(res, node); + Assertions.assertTrue(fail == TestResultType.OK, String.format("Expected exception didn't occur executing %s", expression)); + } catch (Exception e) { + Assertions.assertTrue(fail == TestResultType.EXECUTION, String.format("Unexpected exception executing %s: " + e.getMessage(), expression)); + node = null; + } } - if ("true".equals(test.getAttribute("predicate"))) { - boolean ok = fp.convertToBoolean(outcome); - outcome.clear(); - outcome.add(new BooleanType(ok)); - } - System.out.println(name); if (fp.hasLog()) { System.out.println(name); System.out.println(fp.takeLog()); } - List expected = new ArrayList(); - XMLUtil.getNamedChildren(test, "output", expected); - Assertions.assertEquals(outcome.size(), expected.size(), String.format("Expected %d objects but found %d for expression %s", expected.size(), outcome.size(), expression)); - if ("false".equals(test.getAttribute("ordered"))) { - for (int i = 0; i < Math.min(outcome.size(), expected.size()); i++) { - String tn = outcome.get(i).fhirType(); - String s; - if (outcome.get(i) instanceof Quantity) { - s = fp.convertToString(outcome.get(i)); - } else { - s = ((PrimitiveType) outcome.get(i)).asStringValue(); - } - boolean found = false; - for (Element e : expected) { - if ((Utilities.noString(e.getAttribute("type")) || e.getAttribute("type").equals(tn)) && - (Utilities.noString(e.getTextContent()) || e.getTextContent().equals(s))) { - found = true; - } - } - Assertions.assertTrue(found, String.format("Outcome %d: Value %s of type %s not expected for %s", i, s, tn, expression)); + if (node != null) { + if ("true".equals(test.getAttribute("predicate"))) { + boolean ok = fp.convertToBoolean(outcome); + outcome.clear(); + outcome.add(new BooleanType(ok)); } - } else { - for (int i = 0; i < Math.min(outcome.size(), expected.size()); i++) { - String tn = expected.get(i).getAttribute("type"); - if (!Utilities.noString(tn)) { - Assertions.assertEquals(tn, outcome.get(i).fhirType(), String.format("Outcome %d: Type should be %s but was %s", i, tn, outcome.get(i).fhirType())); - } - String v = expected.get(i).getTextContent(); - if (!Utilities.noString(v)) { + + List expected = new ArrayList(); + XMLUtil.getNamedChildren(test, "output", expected); + Assertions.assertEquals(outcome.size(), expected.size(), String.format("Expected %d objects but found %d for expression %s", expected.size(), outcome.size(), expression)); + if ("false".equals(test.getAttribute("ordered"))) { + for (int i = 0; i < Math.min(outcome.size(), expected.size()); i++) { + String tn = outcome.get(i).fhirType(); + String s; if (outcome.get(i) instanceof Quantity) { - Quantity q = fp.parseQuantityString(v); - Assertions.assertTrue(outcome.get(i).equalsDeep(q), String.format("Outcome %d: Value should be %s but was %s", i, v, outcome.get(i).toString())); + s = fp.convertToString(outcome.get(i)); } else { - Assertions.assertTrue(outcome.get(i) instanceof PrimitiveType, String.format("Outcome %d: Value should be a primitive type but was %s", i, outcome.get(i).fhirType())); - if (!(v.equals(((PrimitiveType) outcome.get(i)).asStringValue()))) { - System.out.println(name); - System.out.println(String.format("Outcome %d: Value should be %s but was %s for expression %s", i, v, outcome.get(i).toString(), expression)); + s = ((PrimitiveType) outcome.get(i)).asStringValue(); + } + boolean found = false; + for (Element e : expected) { + if ((Utilities.noString(e.getAttribute("type")) || e.getAttribute("type").equals(tn)) && + (Utilities.noString(e.getTextContent()) || e.getTextContent().equals(s))) { + found = true; + } + } + Assertions.assertTrue(found, String.format("Outcome %d: Value %s of type %s not expected for %s", i, s, tn, expression)); + } + } else { + for (int i = 0; i < Math.min(outcome.size(), expected.size()); i++) { + String tn = expected.get(i).getAttribute("type"); + if (!Utilities.noString(tn)) { + Assertions.assertEquals(tn, outcome.get(i).fhirType(), String.format("Outcome %d: Type should be %s but was %s", i, tn, outcome.get(i).fhirType())); + } + String v = expected.get(i).getTextContent(); + if (!Utilities.noString(v)) { + if (outcome.get(i) instanceof Quantity) { + Quantity q = fp.parseQuantityString(v); + Assertions.assertTrue(outcome.get(i).equalsDeep(q), String.format("Outcome %d: Value should be %s but was %s", i, v, outcome.get(i).toString())); + } else { + Assertions.assertTrue(outcome.get(i) instanceof PrimitiveType, String.format("Outcome %d: Value should be a primitive type but was %s", i, outcome.get(i).fhirType())); + if (!(v.equals(((PrimitiveType) outcome.get(i)).asStringValue()))) { + System.out.println(name); + System.out.println(String.format("Outcome %d: Value should be %s but was %s for expression %s", i, v, outcome.get(i).toString(), expression)); + } + Assertions.assertEquals(v, ((PrimitiveType) outcome.get(i)).asStringValue(), String.format("Outcome %d: Value should be %s but was %s for expression %s", i, v, outcome.get(i).toString(), expression)); } - Assertions.assertEquals(v, ((PrimitiveType) outcome.get(i)).asStringValue(), String.format("Outcome %d: Value should be %s but was %s for expression %s", i, v, outcome.get(i).toString(), expression)); } } } diff --git a/pom.xml b/pom.xml index 29be4c280..b2ba8a38e 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ 5.1.0 - 1.1.40 + 1.1.41 5.6.2 3.0.0-M4 0.8.5