From a3c32d86a002786098820afa3bf75062a5538af0 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Wed, 26 Jul 2023 12:32:51 +1000 Subject: [PATCH 1/7] add support for Liquid assign + Fix JSON unicode encoding and add character check in validator for illegal XML Unicode characters --- .../org/hl7/fhir/r5/utils/LiquidEngine.java | 114 ++++++++++++++---- .../r5/formats/UnicodeCharacterTests.java | 44 +++++++ .../org/hl7/fhir/utilities/Utilities.java | 2 +- .../fhir/utilities/i18n/I18nConstants.java | 18 +++ .../src/main/resources/Messages.properties | 18 +++ .../instance/InstanceValidator.java | 8 ++ 6 files changed, 177 insertions(+), 27 deletions(-) create mode 100644 org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/formats/UnicodeCharacterTests.java diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/LiquidEngine.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/LiquidEngine.java index e4bbf2103..95221f649 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/LiquidEngine.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/LiquidEngine.java @@ -47,6 +47,7 @@ import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.r5.utils.FHIRPathEngine.ExpressionNodeWithOffset; import org.hl7.fhir.r5.utils.FHIRPathEngine.IEvaluationContext; import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.i18n.I18nConstants; import org.hl7.fhir.utilities.xhtml.NodeType; import org.hl7.fhir.utilities.xhtml.XhtmlNode; @@ -67,17 +68,27 @@ public class LiquidEngine implements IEvaluationContext { private class LiquidEngineContext { private Object externalContext; - private Map vars = new HashMap<>(); + private Map loopVars = new HashMap<>(); + private Map globalVars = new HashMap<>(); public LiquidEngineContext(Object externalContext) { super(); this.externalContext = externalContext; + globalVars = new HashMap<>(); + } + + public LiquidEngineContext(Object externalContext, LiquidEngineContext existing) { + super(); + this.externalContext = externalContext; + loopVars.putAll(existing.loopVars); + globalVars = existing.globalVars; } public LiquidEngineContext(LiquidEngineContext existing) { super(); externalContext = existing.externalContext; - vars.putAll(existing.vars); + loopVars.putAll(existing.loopVars); + globalVars = existing.globalVars; } } @@ -185,7 +196,7 @@ public class LiquidEngine implements IEvaluationContext { String f = lexer.getCurrent(); LiquidFilter filter = LiquidFilter.fromCode(f); if (filter == null) { - lexer.error("Unknown Liquid filter '"+f+"'"); + lexer.error(engine.getWorker().formatMessage(I18nConstants.LIQUID_UNKNOWN_FILTER, f)); } lexer.next(); if (!lexer.done() && lexer.getCurrent().equals(":")) { @@ -195,7 +206,7 @@ public class LiquidEngine implements IEvaluationContext { compiled.add(new LiquidExpressionNode(filter, null)); } } else { - lexer.error("Unexpected syntax parsing liquid statement"); + lexer.error(engine.getWorker().formatMessage(I18nConstants.LIQUID_UNKNOWN_SYNTAX)); } } } @@ -305,6 +316,30 @@ public class LiquidEngine implements IEvaluationContext { } } + private class LiquidAssign extends LiquidNode { + private String varName; + private String expression; + private ExpressionNode compiled; + @Override + public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException { + if (compiled == null) { + boolean dbl = engine.isAllowDoubleQuotes(); + engine.setAllowDoubleQuotes(true); + ExpressionNodeWithOffset po = engine.parsePartial(expression, 0); + compiled = po.getNode(); + engine.setAllowDoubleQuotes(dbl); + } + List list = engine.evaluate(ctxt, resource, resource, resource, compiled); + if (list.isEmpty()) { + ctxt.globalVars.remove(varName); + } else if (list.size() == 1) { + ctxt.globalVars.put(varName, list.get(0)); + } else { + throw new Error("Assign returned a list?"); + } + } + } + private class LiquidFor extends LiquidNode { private String varName; private String condition; @@ -343,7 +378,10 @@ public class LiquidEngine implements IEvaluationContext { if (limit >= 0 && i == limit) { break; } - lctxt.vars.put(varName, o); + if (lctxt.globalVars.containsKey(varName)) { + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_VARIABLE_ALREADY_ASSIGNED, varName)); + } + lctxt.loopVars.put(varName, o); boolean wantBreak = false; for (LiquidNode n : body) { try { @@ -372,7 +410,7 @@ public class LiquidEngine implements IEvaluationContext { } else if (cnt.startsWith("limit")) { cnt = cnt.substring(5).trim(); if (!cnt.startsWith(":")) { - throw new FHIRException("Exception evaluating "+src+": limit is not followed by ':'"); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_COLON, src)); } cnt = cnt.substring(1).trim(); int i = 0; @@ -380,14 +418,14 @@ public class LiquidEngine implements IEvaluationContext { i++; } if (i == 0) { - throw new FHIRException("Exception evaluating "+src+": limit is not followed by a number"); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_NUMBER, src)); } limit = Integer.parseInt(cnt.substring(0, i)); cnt = cnt.substring(i); } else if (cnt.startsWith("offset")) { cnt = cnt.substring(6).trim(); if (!cnt.startsWith(":")) { - throw new FHIRException("Exception evaluating "+src+": limit is not followed by ':'"); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_COLON, src)); } cnt = cnt.substring(1).trim(); int i = 0; @@ -395,12 +433,12 @@ public class LiquidEngine implements IEvaluationContext { i++; } if (i == 0) { - throw new FHIRException("Exception evaluating "+src+": limit is not followed by a number"); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_NUMBER, src)); } offset = Integer.parseInt(cnt.substring(0, i)); cnt = cnt.substring(i); } else { - throw new FHIRException("Exception evaluating "+src+": unexpected content at "+cnt); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_UNEXPECTED, cnt)); } } } @@ -415,9 +453,9 @@ public class LiquidEngine implements IEvaluationContext { String src = includeResolver.fetchInclude(LiquidEngine.this, page); LiquidParser parser = new LiquidParser(src); LiquidDocument doc = parser.parse(page); - LiquidEngineContext nctxt = new LiquidEngineContext(ctxt.externalContext); + LiquidEngineContext nctxt = new LiquidEngineContext(ctxt.externalContext, ctxt); Tuple incl = new Tuple(); - nctxt.vars.put("include", incl); + nctxt.loopVars.put("include", incl); for (String s : params.keySet()) { incl.addProperty(s, engine.evaluate(ctxt, resource, resource, resource, params.get(s))); } @@ -474,11 +512,11 @@ public class LiquidEngine implements IEvaluationContext { cnt = "," + cnt.substring(5).trim(); while (!Utilities.noString(cnt)) { if (!cnt.startsWith(",")) { - throw new FHIRException("Script " + name + ": Script " + name + ": Found " + cnt.charAt(0) + " expecting ',' parsing cycle"); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_EXPECTING, name, cnt.charAt(0), ',')); } cnt = cnt.substring(1).trim(); if (!cnt.startsWith("\"")) { - throw new FHIRException("Script " + name + ": Script " + name + ": Found " + cnt.charAt(0) + " expecting '\"' parsing cycle"); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_EXPECTING, name, cnt.charAt(0), '"')); } cnt = cnt.substring(1); int i = 0; @@ -486,7 +524,7 @@ public class LiquidEngine implements IEvaluationContext { i++; } if (i == cnt.length()) { - throw new FHIRException("Script " + name + ": Script " + name + ": Found unterminated string parsing cycle"); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_UNTERMINATED, name)); } res.list.add(cnt.substring(0, i)); cnt = cnt.substring(i + 1).trim(); @@ -518,8 +556,10 @@ public class LiquidEngine implements IEvaluationContext { list.add(parseCycle(cnt)); else if (cnt.startsWith("include ")) list.add(parseInclude(cnt.substring(7).trim())); + else if (cnt.startsWith("assign ")) + list.add(parseAssign(cnt.substring(6).trim())); else - throw new FHIRException("Script " + name + ": Script " + name + ": Unknown flow control statement " + cnt); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_UNKNOWN_FLOW_STMT,name, cnt)); } else { // next2() == '{' list.add(parseStatement()); } @@ -533,7 +573,7 @@ public class LiquidEngine implements IEvaluationContext { n.closeUp(); if (terminators.length > 0) if (!isTerminator(close, terminators)) - throw new FHIRException("Script " + name + ": Script " + name + ": Found end of script looking for " + terminators); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_UNKNOWN_NOEND, name, terminators)); return close; } @@ -577,7 +617,7 @@ public class LiquidEngine implements IEvaluationContext { while (i < cnt.length() && !Character.isWhitespace(cnt.charAt(i))) i++; if (i == cnt.length() || i == 0) - throw new FHIRException("Script " + name + ": Error reading include: " + cnt); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_INCLUDE, name + ": Error reading include: " + cnt)); LiquidInclude res = new LiquidInclude(); res.page = cnt.substring(0, i); while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i))) @@ -587,10 +627,10 @@ public class LiquidEngine implements IEvaluationContext { while (i < cnt.length() && cnt.charAt(i) != '=') i++; if (i >= cnt.length() || j == i) - throw new FHIRException("Script " + name + ": Error reading include: " + cnt); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_INCLUDE, name, cnt)); String n = cnt.substring(j, i); if (res.params.containsKey(n)) - throw new FHIRException("Script " + name + ": Error reading include: " + cnt); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_INCLUDE, name, cnt)); i++; ExpressionNodeWithOffset t = engine.parsePartial(cnt, i); i = t.getOffset(); @@ -607,13 +647,16 @@ public class LiquidEngine implements IEvaluationContext { i++; LiquidFor res = new LiquidFor(); res.varName = cnt.substring(0, i); + if ("include".equals(res.varName)) { + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_VARIABLE_ILLEGAL, res.varName)); + } while (Character.isWhitespace(cnt.charAt(i))) i++; int j = i; while (!Character.isWhitespace(cnt.charAt(i))) i++; if (!"in".equals(cnt.substring(j, i))) - throw new FHIRException("Script " + name + ": Script " + name + ": Error reading loop: " + cnt); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_LOOP, name, cnt)); res.condition = cnt.substring(i).trim(); parseList(res.body, false, new String[] { "endloop" }); return res; @@ -625,13 +668,16 @@ public class LiquidEngine implements IEvaluationContext { i++; LiquidFor res = new LiquidFor(); res.varName = cnt.substring(0, i); + if ("include".equals(res.varName)) { + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_VARIABLE_ILLEGAL, res.varName)); + } while (Character.isWhitespace(cnt.charAt(i))) i++; int j = i; while (!Character.isWhitespace(cnt.charAt(i))) i++; if (!"in".equals(cnt.substring(j, i))) - throw new FHIRException("Script " + name + ": Script " + name + ": Error reading loop: " + cnt); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_LOOP, name, cnt)); res.condition = cnt.substring(i).trim(); String term = parseList(res.body, true, new String[] { "endfor", "else" }); if ("else".equals(term)) { @@ -640,6 +686,20 @@ public class LiquidEngine implements IEvaluationContext { return res; } + private LiquidNode parseAssign(String cnt) throws FHIRException { + int i = 0; + while (!Character.isWhitespace(cnt.charAt(i))) + i++; + LiquidAssign res = new LiquidAssign(); + res.varName = cnt.substring(0, i); + while (Character.isWhitespace(cnt.charAt(i))) + i++; + int j = i; + while (!Character.isWhitespace(cnt.charAt(i))) + i++; + res.expression = cnt.substring(i).trim(); + return res; + } private String parseTag(char ch) throws FHIRException { grab(); @@ -649,7 +709,7 @@ public class LiquidEngine implements IEvaluationContext { b.append(grab()); } if (!(next1() == '%' && next2() == '}')) - throw new FHIRException("Script " + name + ": Unterminated Liquid statement {% " + b.toString()); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_NOTERM, name, "{% " + b.toString())); grab(); grab(); return b.toString().trim(); @@ -663,7 +723,7 @@ public class LiquidEngine implements IEvaluationContext { b.append(grab()); } if (!(next1() == '}' && next2() == '}')) - throw new FHIRException("Script " + name + ": Unterminated Liquid statement {{ " + b.toString()); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_NOTERM, name, "{{ " + b.toString())); grab(); grab(); LiquidStatement res = new LiquidStatement(); @@ -676,8 +736,10 @@ public class LiquidEngine implements IEvaluationContext { @Override public List resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException { LiquidEngineContext ctxt = (LiquidEngineContext) appContext; - if (ctxt.vars.containsKey(name)) - return new ArrayList(Arrays.asList(ctxt.vars.get(name))); + if (ctxt.loopVars.containsKey(name)) + return new ArrayList(Arrays.asList(ctxt.loopVars.get(name))); + if (ctxt.globalVars.containsKey(name)) + return new ArrayList(Arrays.asList(ctxt.globalVars.get(name))); if (externalHostServices == null) return new ArrayList(); return externalHostServices.resolveConstant(ctxt.externalContext, name, beforeContext); diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/formats/UnicodeCharacterTests.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/formats/UnicodeCharacterTests.java new file mode 100644 index 000000000..4294aba95 --- /dev/null +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/formats/UnicodeCharacterTests.java @@ -0,0 +1,44 @@ +package org.hl7.fhir.r5.formats; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +import org.hl7.fhir.exceptions.FHIRFormatError; +import org.hl7.fhir.r5.formats.IParser.OutputStyle; +import org.hl7.fhir.r5.model.Parameters; +import org.hl7.fhir.r5.test.utils.TestingUtilities; +import org.hl7.fhir.utilities.Utilities; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class UnicodeCharacterTests { + + @Test + public void testUnicodeXml() throws FHIRFormatError, IOException { + XmlParser xml = new XmlParser(); + xml.setOutputStyle(OutputStyle.PRETTY); + Parameters p = (Parameters) xml.parse(TestingUtilities.loadTestResource("r5", "unicode-problem.xml")); + Assertions.assertEquals("invalid: \u0013, not invalid: \r", p.getParameterFirstRep().getValue().primitiveValue()); + FileOutputStream o = new FileOutputStream(Utilities.path("[tmp]", "unicode-problem.xml")); + xml.compose(o, p); + o.close(); + p = (Parameters) xml.parse(new FileInputStream(Utilities.path("[tmp]", "unicode-problem.xml"))); + Assertions.assertEquals("invalid: \u0013, not invalid: \r", p.getParameterFirstRep().getValue().primitiveValue()); + } + + + @Test + public void testUnicodeJson() throws FHIRFormatError, IOException { + JsonParser json = new JsonParser(); + json.setOutputStyle(OutputStyle.PRETTY); + Parameters p = (Parameters) json.parse(TestingUtilities.loadTestResource("r5", "unicode-problem.json")); + Assertions.assertEquals("invalid: \u0013, not invalid: \r", p.getParameterFirstRep().getValue().primitiveValue()); + FileOutputStream o = new FileOutputStream(Utilities.path("[tmp]", "unicode-problem.json")); + json.compose(o, p); + o.close(); + p = (Parameters) json.parse(new FileInputStream(Utilities.path("[tmp]", "unicode-problem.json"))); + Assertions.assertEquals("invalid: \u0013, not invalid: \r", p.getParameterFirstRep().getValue().primitiveValue()); + } + +} diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java index 6e727dc45..c177c262a 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java @@ -984,7 +984,7 @@ public class Utilities { else if (isWhitespace(c)) { b.append("\\u"+Utilities.padLeft(Integer.toHexString(c), '0', 4)); } else if (((int) c) < 32) - b.append("\\u" + Utilities.padLeft(String.valueOf((int) c), '0', 4)); + b.append("\\u" + Utilities.padLeft(Integer.toHexString(c), '0', 4)); else b.append(c); } diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java index a0e331ad9..378547f2e 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java @@ -926,6 +926,24 @@ public class I18nConstants { public static final String VALUESET_CONCEPT_DISPLAY_PRESENCE_MIXED = "VALUESET_CONCEPT_DISPLAY_PRESENCE_MIXED"; public static final String VALUESET_CONCEPT_DISPLAY_SCT_TAG_MIXED = "VALUESET_CONCEPT_DISPLAY_SCT_TAG_MIXED"; public static final String CS_SCT_IPS_NOT_IPS = "CS_SCT_IPS_NOT_IPS"; + public static final String UNICODE_XML_BAD_CHARS = "UNICODE_XML_BAD_CHARS"; + public static final String LIQUID_UNKNOWN_FILTER = "LIQUID_UNKNOWN_FILTER"; + public static final String LIQUID_UNKNOWN_SYNTAX = "LIQUID_UNKNOWN_SYNTAX"; + public static final String LIQUID_SYNTAX_EXPECTING = "LIQUID_SYNTAX_EXPECTING"; + public static final String LIQUID_SYNTAX_UNTERMINATED = "LIQUID_SYNTAX_UNTERMINATED"; + public static final String LIQUID_UNKNOWN_FLOW_STMT = "LIQUID_UNKNOWN_FLOW_STMT"; + public static final String LIQUID_UNKNOWN_NOEND = "LIQUID_UNKNOWN_NOEND"; + public static final String LIQUID_SYNTAX_INCLUDE = "LIQUID_SYNTAX_INCLUDE"; + public static final String LIQUID_SYNTAX_LOOP = "LIQUID_SYNTAX_LOOP"; + public static final String LIQUID_SYNTAX_NOTERM = "LIQUID_SYNTAX_NOTERM"; + public static final String LIQUID_UNKNOWN_NOTERM = "LIQUID_UNKNOWN_NOTERM"; + public static final String LIQUID_SYNTAX_COLON = "LIQUID_SYNTAX_COLON"; + public static final String LIQUID_SYNTAX_NUMBER = "LIQUID_SYNTAX_NUMBER"; + public static final String LIQUID_SYNTAX_UNEXPECTED = "LIQUID_SYNTAX_UNEXPECTED"; + public static final String LIQUID_VARIABLE_ALREADY_ASSIGNED = "LIQUID_VARIABLE_ALREADY_ASSIGNED"; + public static final String LIQUID_VARIABLE_ILLEGAL = "LIQUID_VARIABLE_ILLEGAL"; + + } diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index 58d7768db..34cbf51a4 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -981,3 +981,21 @@ SD_ED_TYPE_WRONG_TYPE_other = The element has a type {0} which is not in the typ VALUESET_CONCEPT_DISPLAY_PRESENCE_MIXED = This include has some concepts with displays and some without - check that this is what is intended VALUESET_CONCEPT_DISPLAY_SCT_TAG_MIXED = This SNOMED-CT based include has some concepts with semantic tags (FSN terms) and some without (preferred terms) - check that this is what is intended CS_SCT_IPS_NOT_IPS = The Snomed CT code {0} ({1}) is not a member of the IPS free set +UNICODE_XML_BAD_CHARS_one = This content includes the character {1} (hex value). This character is illegal in the XML version of FHIR, and there is generally no valid use for such characters +UNICODE_XML_BAD_CHARS_other = This content includes the characters {1} (hex values). These characters are illegal in the XML version of FHIR, and there is generally no valid use for such characters +LIQUID_UNKNOWN_FILTER = Unknown Liquid filter '''{0}'' +LIQUID_UNKNOWN_SYNTAX = Unexpected syntax parsing liquid statement +LIQUID_SYNTAX_EXPECTING = Script {0}: Found ''{1}'' expecting ''{2}'' parsing cycle +LIQUID_SYNTAX_UNTERMINATED = Script {0}: Found unterminated string parsing cycle +LIQUID_UNKNOWN_FLOW_STMT = Script {0}: Unknown flow control statement ''{1}'' +LIQUID_UNKNOWN_NOEND = Script {0}: Found end of script looking for {1} +LIQUID_SYNTAX_INCLUDE, = Script {0}: Error reading include: {1} +LIQUID_SYNTAX_LOOP = Script {0}: Error reading loop: {1} +LIQUID_SYNTAX_NOTERM = Script {0}: Unterminated Liquid statement {1} +LIQUID_UNKNOWN_NOTERM = Script {0}: Unterminated Liquid statement {1} +LIQUID_SYNTAX_COLON = Exception evaluating {0}: limit is not followed by '':'' +LIQUID_SYNTAX_NUMBER = Exception evaluating {0}: limit is not followed by a number +LIQUID_SYNTAX_UNEXPECTED = Exception evaluating {0}: unexpected content at {1} +LIQUID_VARIABLE_ALREADY_ASSIGNED = "Liquid Exception: The variable ''{0}'' already has an assigned value +LIQUID_VARIABLE_ILLEGAL = "Liquid Exception: The variable name ''{0}'' cannot be used + \ No newline at end of file diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java index bbc993721..a2391d32a 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java @@ -2379,6 +2379,14 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat ok = false; } } + Set badChars = new HashSet<>(); + for (char ch : e.primitiveValue().toCharArray()) { + if (ch < 32 && !(ch == '\r' || ch == '\n' || ch == '\t')) { + // can't get to here with xml - the parser fails if you try + badChars.add(Integer.toHexString(ch)); + } + } + warningPlural(errors, "2023-07-26", IssueType.INVALID, e.line(), e.col(), path, badChars.isEmpty(), badChars.size(), I18nConstants.UNICODE_XML_BAD_CHARS, badChars.toString()); } String regex = context.getExtensionString(ToolingExtensions.EXT_REGEX); // there's a messy history here - this extension snhould only be on the element definition itself, but for historical reasons From 6afb151dffd9fe0d823a79d3d373e812a8ebf3c4 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Thu, 27 Jul 2023 17:49:03 +1000 Subject: [PATCH 2/7] Better information when CodeSystem.content = not-present --- .../org/hl7/fhir/utilities/i18n/I18nConstants.java | 1 + .../src/main/resources/Messages.properties | 1 + .../fhir/validation/instance/InstanceValidator.java | 10 +++++++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java index 378547f2e..77f2afc9b 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java @@ -942,6 +942,7 @@ public class I18nConstants { public static final String LIQUID_SYNTAX_UNEXPECTED = "LIQUID_SYNTAX_UNEXPECTED"; public static final String LIQUID_VARIABLE_ALREADY_ASSIGNED = "LIQUID_VARIABLE_ALREADY_ASSIGNED"; public static final String LIQUID_VARIABLE_ILLEGAL = "LIQUID_VARIABLE_ILLEGAL"; + public static final String TERMINOLOGY_TX_SYSTEM_NOT_USABLE = "TERMINOLOGY_TX_SYSTEM_NOT_USABLE"; diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index 34cbf51a4..530ec44f4 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -184,6 +184,7 @@ Terminology_TX_NoValid_8 = None of the codes provided are in the maximum value s Terminology_TX_NoValid_9 = The code provided ({2}) could not be validated against the maximum value set {0}, (error = {1}) Terminology_TX_System_Invalid = Invalid System URI: {0} Terminology_TX_System_NotKnown = Code System URI ''{0}'' is unknown so the code cannot be validated +TERMINOLOGY_TX_SYSTEM_NOT_USABLE = Code System with URI ''{0}'' has no content so the code cannot be validated Terminology_TX_System_Relative = Coding.system must be an absolute reference, not a local reference Terminology_TX_System_Unknown = Unknown Code System ''{0}'' Terminology_TX_System_ValueSet = Invalid System URI: {0} - cannot use a value set URI as a system diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java index a2391d32a..206a72cfc 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java @@ -113,6 +113,7 @@ import org.hl7.fhir.r5.model.ElementDefinition.PropertyRepresentation; import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent; import org.hl7.fhir.r5.model.Enumeration; import org.hl7.fhir.r5.model.Enumerations.BindingStrength; +import org.hl7.fhir.r5.model.Enumerations.CodeSystemContentMode; import org.hl7.fhir.r5.model.ExpressionNode; import org.hl7.fhir.r5.model.ExpressionNode.CollectionStatus; import org.hl7.fhir.r5.model.Extension; @@ -1075,7 +1076,14 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } } if (!isAllowExamples() || !Utilities.startsWithInList(system, "http://example.org", "https://example.org")) { - hint(errors, NO_RULE_DATE, IssueType.UNKNOWN, element.line(), element.col(), path, done, I18nConstants.TERMINOLOGY_TX_SYSTEM_NOTKNOWN, system); + CodeSystem cs = context.fetchCodeSystem(system); + if (cs == null) { + hint(errors, NO_RULE_DATE, IssueType.UNKNOWN, element.line(), element.col(), path, done, I18nConstants.TERMINOLOGY_TX_SYSTEM_NOTKNOWN, system); + } else { + if (hint(errors, NO_RULE_DATE, IssueType.UNKNOWN, element.line(), element.col(), path, cs.getContent() != CodeSystemContentMode.NOTPRESENT, I18nConstants.TERMINOLOGY_TX_SYSTEM_NOT_USABLE, system)) { + rule(errors, NO_RULE_DATE, IssueType.UNKNOWN, element.line(), element.col(), path, false, "Error - this should not happen? (Consult GG)"); + } + } } return true; } catch (Exception e) { From 4eff9a3b68e34ec642ab49b9aa3ada4f06d82d01 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Thu, 27 Jul 2023 17:49:57 +1000 Subject: [PATCH 3/7] fix up validation of FHIRPaths in differentials, and add warnings when collection status isn't right --- .../org/hl7/fhir/r5/model/ExpressionNode.java | 4 + .../org/hl7/fhir/r5/model/TypeDetails.java | 5 +- .../org/hl7/fhir/r5/utils/FHIRPathEngine.java | 74 ++++++++++++++++--- .../fhir/utilities/i18n/I18nConstants.java | 3 + .../src/main/resources/Messages.properties | 12 +-- .../src/main/resources/Messages_es.properties | 3 + 6 files changed, 83 insertions(+), 18 deletions(-) diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/ExpressionNode.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/ExpressionNode.java index e5af0adef..5330ca00b 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/ExpressionNode.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/ExpressionNode.java @@ -359,6 +359,10 @@ public class ExpressionNode { public enum CollectionStatus { SINGLETON, ORDERED, UNORDERED; + + boolean isList() { + return this == ORDERED || this == UNORDERED; + } } //the expression will have one of either name or constant diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/TypeDetails.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/TypeDetails.java index d3a122dca..f0d0b02e1 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/TypeDetails.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/TypeDetails.java @@ -285,7 +285,7 @@ public class TypeDetails { public void update(TypeDetails source) { for (ProfiledType pt : source.types) addType(pt); - if (collectionStatus == null) + if (collectionStatus == null || collectionStatus == CollectionStatus.SINGLETON) collectionStatus = source.collectionStatus; else if (source.collectionStatus == CollectionStatus.UNORDERED) collectionStatus = source.collectionStatus; @@ -516,5 +516,8 @@ public class TypeDetails { public static TypeDetails empty() { return new TypeDetails(CollectionStatus.SINGLETON); } + public boolean isList() { + return collectionStatus != null && collectionStatus.isList(); + } } \ No newline at end of file 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 657bd6c1f..e760163df 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 @@ -281,6 +281,7 @@ public class FHIRPathEngine { private boolean doNotEnforceAsSingletonRule; private boolean doNotEnforceAsCaseSensitive; private boolean allowDoubleQuotes; + private List typeWarnings = new ArrayList<>(); // 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 @@ -656,7 +657,8 @@ public class FHIRPathEngine { * @throws PathEngineException * @if the path is not valid */ - public TypeDetails checkOnTypes(Object appContext, String resourceType, List typeList, ExpressionNode expr) throws FHIRLexerException, PathEngineException, DefinitionException { + public TypeDetails checkOnTypes(Object appContext, String resourceType, List typeList, ExpressionNode expr, List warnings) throws FHIRLexerException, PathEngineException, DefinitionException { + typeWarnings.clear(); // if context is a path that refers to a type, do that conversion now TypeDetails types = new TypeDetails(CollectionStatus.SINGLETON); @@ -689,7 +691,9 @@ public class FHIRPathEngine { } } } - return executeType(new ExecutionTypeContext(appContext, resourceType, types, types), types, expr, null, true, false); + TypeDetails res = executeType(new ExecutionTypeContext(appContext, resourceType, types, types), types, expr, null, true, false); + warnings.addAll(typeWarnings); + return res; } /** @@ -2017,18 +2021,54 @@ public class FHIRPathEngine { } + private void checkCardinalityForComparabilitySame(TypeDetails left, Operation operation, TypeDetails right, ExpressionNode expr) { + if (left.isList() && !right.isList()) { + typeWarnings.add(worker.formatMessage(I18nConstants.FHIRPATH_COLLECTION_STATUS_OPERATION_LEFT, expr.toString())); + } else if (!left.isList() && right.isList()) { + typeWarnings.add(worker.formatMessage(I18nConstants.FHIRPATH_COLLECTION_STATUS_OPERATION_RIGHT, expr.toString())); + } + } + + private void checkCardinalityForSingle(TypeDetails left, Operation operation, TypeDetails right, ExpressionNode expr) { + if (left.isList()) { + typeWarnings.add(worker.formatMessage(I18nConstants.FHIRPATH_COLLECTION_STATUS_OPERATION_LEFT, expr.toString())); + } + if (right.isList()) { + typeWarnings.add(worker.formatMessage(I18nConstants.FHIRPATH_COLLECTION_STATUS_OPERATION_RIGHT, expr.toString())); + } + } + private TypeDetails operateTypes(TypeDetails left, Operation operation, TypeDetails right, ExpressionNode expr) { switch (operation) { - case Equals: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case Equivalent: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case NotEquals: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case NotEquivalent: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case LessThan: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case Greater: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case LessOrEqual: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case GreaterOrEqual: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case Is: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case Equals: + checkCardinalityForComparabilitySame(left, operation, right, expr); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case Equivalent: + checkCardinalityForComparabilitySame(left, operation, right, expr); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case NotEquals: + checkCardinalityForComparabilitySame(left, operation, right, expr); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case NotEquivalent: + checkCardinalityForComparabilitySame(left, operation, right, expr); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case LessThan: + checkCardinalityForSingle(left, operation, right, expr); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case Greater: + checkCardinalityForSingle(left, operation, right, expr); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case LessOrEqual: + checkCardinalityForSingle(left, operation, right, expr); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case GreaterOrEqual: + checkCardinalityForSingle(left, operation, right, expr); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case Is: + checkCardinalityForSingle(left, operation, right, expr); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); case As: + checkCardinalityForSingle(left, operation, right, expr); TypeDetails td = new TypeDetails(CollectionStatus.SINGLETON, right.getTypes()); if (td.typesHaveTargets()) { td.addTargets(left.getTargets()); @@ -2040,6 +2080,7 @@ public class FHIRPathEngine { case Xor: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); case Implies : return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); case Times: + checkCardinalityForSingle(left, operation, right, expr); TypeDetails result = new TypeDetails(CollectionStatus.SINGLETON); if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) { result.addType(TypeDetails.FP_Integer); @@ -2048,6 +2089,7 @@ public class FHIRPathEngine { } return result; case DivideBy: + checkCardinalityForSingle(left, operation, right, expr); result = new TypeDetails(CollectionStatus.SINGLETON); if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) { result.addType(TypeDetails.FP_Decimal); @@ -2056,9 +2098,11 @@ public class FHIRPathEngine { } return result; case Concatenate: + checkCardinalityForSingle(left, operation, right, expr); result = new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); return result; case Plus: + checkCardinalityForSingle(left, operation, right, expr); result = new TypeDetails(CollectionStatus.SINGLETON); if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) { result.addType(TypeDetails.FP_Integer); @@ -2075,6 +2119,7 @@ public class FHIRPathEngine { } return result; case Minus: + checkCardinalityForSingle(left, operation, right, expr); result = new TypeDetails(CollectionStatus.SINGLETON); if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) { result.addType(TypeDetails.FP_Integer); @@ -2092,6 +2137,7 @@ public class FHIRPathEngine { return result; case Div: case Mod: + checkCardinalityForSingle(left, operation, right, expr); result = new TypeDetails(CollectionStatus.SINGLETON); if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) { result.addType(TypeDetails.FP_Integer); @@ -3222,7 +3268,7 @@ public class FHIRPathEngine { if (atEntry && Character.isUpperCase(exp.getName().charAt(0)) && hashTail(type).equals(exp.getName())) { // special case for start up return new TypeDetails(CollectionStatus.SINGLETON, type); } - TypeDetails result = new TypeDetails(null); + TypeDetails result = new TypeDetails(focus.getCollectionStatus()); getChildTypesByName(type, exp.getName(), result, exp, focus, elementDependencies); return result; } @@ -3265,6 +3311,10 @@ public class FHIRPathEngine { } while (changed); paramTypes.clear(); paramTypes.add(base); + } else if (exp.getFunction() == Function.Where || exp.getFunction() == Function.Select || exp.getFunction() == Function.Exists || + exp.getFunction() == Function.All || exp.getFunction() == Function.AllTrue || exp.getFunction() == Function.AnyTrue + || exp.getFunction() == Function.AllFalse || exp.getFunction() == Function.AnyFalse) { + evaluateParameters(context, focus.toSingleton(), exp, elementDependencies, paramTypes, false); } else { evaluateParameters(context, focus, exp, elementDependencies, paramTypes, false); } diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java index 77f2afc9b..2277deed8 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java @@ -943,6 +943,9 @@ public class I18nConstants { public static final String LIQUID_VARIABLE_ALREADY_ASSIGNED = "LIQUID_VARIABLE_ALREADY_ASSIGNED"; public static final String LIQUID_VARIABLE_ILLEGAL = "LIQUID_VARIABLE_ILLEGAL"; public static final String TERMINOLOGY_TX_SYSTEM_NOT_USABLE = "TERMINOLOGY_TX_SYSTEM_NOT_USABLE"; + public static final String ED_INVARIANT_DIFF_NO_SOURCE = "ED_INVARIANT_DIFF_NO_SOURCE"; + public static final String FHIRPATH_COLLECTION_STATUS_OPERATION_LEFT = "FHIRPATH_COLLECTION_STATUS_OPERATION_LEFT"; + public static final String FHIRPATH_COLLECTION_STATUS_OPERATION_RIGHT = "FHIRPATH_COLLECTION_STATUS_OPERATION_RIGHT"; diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index 530ec44f4..c95a25b9f 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -184,7 +184,7 @@ Terminology_TX_NoValid_8 = None of the codes provided are in the maximum value s Terminology_TX_NoValid_9 = The code provided ({2}) could not be validated against the maximum value set {0}, (error = {1}) Terminology_TX_System_Invalid = Invalid System URI: {0} Terminology_TX_System_NotKnown = Code System URI ''{0}'' is unknown so the code cannot be validated -TERMINOLOGY_TX_SYSTEM_NOT_USABLE = Code System with URI ''{0}'' has no content so the code cannot be validated +TERMINOLOGY_TX_SYSTEM_NOT_USABLE = The definition for the Code System with URI ''{0}'' doesn't provide any codes so the code cannot be validated Terminology_TX_System_Relative = Coding.system must be an absolute reference, not a local reference Terminology_TX_System_Unknown = Unknown Code System ''{0}'' Terminology_TX_System_ValueSet = Invalid System URI: {0} - cannot use a value set URI as a system @@ -990,13 +990,15 @@ LIQUID_SYNTAX_EXPECTING = Script {0}: Found ''{1}'' expecting ''{2}'' parsing cy LIQUID_SYNTAX_UNTERMINATED = Script {0}: Found unterminated string parsing cycle LIQUID_UNKNOWN_FLOW_STMT = Script {0}: Unknown flow control statement ''{1}'' LIQUID_UNKNOWN_NOEND = Script {0}: Found end of script looking for {1} -LIQUID_SYNTAX_INCLUDE, = Script {0}: Error reading include: {1} +LIQUID_SYNTAX_INCLUDE = Script {0}: Error reading include: {1} LIQUID_SYNTAX_LOOP = Script {0}: Error reading loop: {1} LIQUID_SYNTAX_NOTERM = Script {0}: Unterminated Liquid statement {1} LIQUID_UNKNOWN_NOTERM = Script {0}: Unterminated Liquid statement {1} LIQUID_SYNTAX_COLON = Exception evaluating {0}: limit is not followed by '':'' LIQUID_SYNTAX_NUMBER = Exception evaluating {0}: limit is not followed by a number LIQUID_SYNTAX_UNEXPECTED = Exception evaluating {0}: unexpected content at {1} -LIQUID_VARIABLE_ALREADY_ASSIGNED = "Liquid Exception: The variable ''{0}'' already has an assigned value -LIQUID_VARIABLE_ILLEGAL = "Liquid Exception: The variable name ''{0}'' cannot be used - \ No newline at end of file +LIQUID_VARIABLE_ALREADY_ASSIGNED = Liquid Exception: The variable ''{0}'' already has an assigned value +LIQUID_VARIABLE_ILLEGAL = Liquid Exception: The variable name ''{0}'' cannot be used +ED_INVARIANT_DIFF_NO_SOURCE = The invariant {0} defined in the differential must have no source, or the source must be the same as the profile +FHIRPATH_COLLECTION_STATUS_OPERATION_LEFT = The left side is inherently a collection, and so the expression ''{0}'' may fail or return false if there is more than one item in the content being evaluated +FHIRPATH_COLLECTION_STATUS_OPERATION_RIGHT = The right side is inherently a collection, and so this expression ''{0}'' may fail or return false if there is more than one item in the content being evaluated diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages_es.properties b/org.hl7.fhir.utilities/src/main/resources/Messages_es.properties index 246ca98c8..b209a888d 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages_es.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages_es.properties @@ -829,3 +829,6 @@ NO_VALID_DISPLAY_FOUND_other = PRIMITIVE_VALUE_ALTERNATIVES_MESSAGE_one = PRIMITIVE_VALUE_ALTERNATIVES_MESSAGE_many = PRIMITIVE_VALUE_ALTERNATIVES_MESSAGE_other = +UNICODE_XML_BAD_CHARS_one = +UNICODE_XML_BAD_CHARS_many = +UNICODE_XML_BAD_CHARS_other = \ No newline at end of file From 774440d0a72d7ee0710effa3ffbdc9931f0e84b5 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Thu, 27 Jul 2023 17:50:08 +1000 Subject: [PATCH 4/7] Fix path problem in NpmPackage --- .../src/main/java/org/hl7/fhir/utilities/npm/NpmPackage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/NpmPackage.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/NpmPackage.java index 2e6b697c3..a9098c50a 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/NpmPackage.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/NpmPackage.java @@ -462,7 +462,7 @@ public class NpmPackage { while ((entry = (TarArchiveEntry) tarIn.getNextEntry()) != null) { String n = entry.getName(); - if (n.contains("..")) { + if (n.contains("/..") || n.contains("../")) { throw new RuntimeException("Entry with an illegal name: " + n); } if (entry.isDirectory()) { From 06c865badf801c73e610badd71cb6a7dfee6ab66 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Thu, 27 Jul 2023 17:50:42 +1000 Subject: [PATCH 5/7] Fix NPE validating codes in value sets with improper expansions --- .../validation/ValueSetValidator.java | 2 +- .../type/StructureDefinitionValidator.java | 91 ++++++++++--------- 2 files changed, 51 insertions(+), 42 deletions(-) diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/validation/ValueSetValidator.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/validation/ValueSetValidator.java index f27b37653..5fe76d5c5 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/validation/ValueSetValidator.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/validation/ValueSetValidator.java @@ -643,7 +643,7 @@ public class ValueSetValidator extends ValueSetProcessBase { private boolean checkExpansion(Coding code, List contains, VersionInfo vi) { for (ValueSetExpansionContainsComponent containsComponent: contains) { - if (containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) { + if (containsComponent.hasSystem() && containsComponent.hasCode() && containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) { vi.setExpansionVersion(containsComponent.getVersion()); return true; } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/StructureDefinitionValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/StructureDefinitionValidator.java index b9a050a62..ab57c52f5 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/StructureDefinitionValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/StructureDefinitionValidator.java @@ -469,57 +469,63 @@ public class StructureDefinitionValidator extends BaseValidator { } // if we see fixed[x] or pattern[x] applied to a repeating element, we'll give the user a hint } - if (snapshot) { // we just don't have enough information to figure out the context in a differential - List constraints = element.getChildrenByName("constraint"); - int cc = 0; - for (Element invariant : constraints) { - ok = validateElementDefinitionInvariant(errors, invariant, stack.push(invariant, cc, null, null), invariantMap, elements, element, element.getNamedChildValue("path"), rootPath, profileUrl) && ok; - cc++; - } - } + List constraints = element.getChildrenByName("constraint"); + int cc = 0; + for (Element invariant : constraints) { + ok = validateElementDefinitionInvariant(errors, invariant, stack.push(invariant, cc, null, null), invariantMap, elements, element, element.getNamedChildValue("path"), rootPath, profileUrl, snapshot) && ok; + cc++; + } return ok; } - private boolean validateElementDefinitionInvariant(List errors, Element invariant, NodeStack stack, Map invariantMap, List elements, Element element, String path, String rootPath, String profileUrl) { + private boolean validateElementDefinitionInvariant(List errors, Element invariant, NodeStack stack, Map invariantMap, List elements, Element element, String path, String rootPath, String profileUrl, boolean snapshot) { boolean ok = true; String key = invariant.getNamedChildValue("key"); String expression = invariant.getNamedChildValue("expression"); String source = invariant.getNamedChildValue("source"); if (warning(errors, "2023-06-19", IssueType.INFORMATIONAL, stack, !Utilities.noString(key), I18nConstants.ED_INVARIANT_NO_KEY)) { if (hint(errors, "2023-06-19", IssueType.INFORMATIONAL, stack, !Utilities.noString(expression) || VersionUtilities.isR5Plus(context.getVersion()), I18nConstants.ED_INVARIANT_NO_EXPRESSION, key)) { // not for R5 - there's an invariant - if (!Utilities.noString(expression)) { - if (invariantMap.containsKey(key)) { - // it's legal - and common - for a list of elemnts to contain the same invariant more than once, but it's not valid if it's not always the same - ok = rule(errors, "2023-06-19", IssueType.INVALID, stack, expression.equals(invariantMap.get(key)) || "ele-1".equals(key), I18nConstants.ED_INVARIANT_EXPRESSION_CONFLICT, key, expression, invariantMap.get(key)); - } else { - invariantMap.put(key, expression); - } - if (Utilities.noString(source) || (source.equals(profileUrl))) { // no need to revalidate FHIRPath from elsewhere - try { - // we have to figure out the context, and we might be in type slicing. - String exp = expression; - Element te = element; - List types = getTypesForElement(elements, te); - while (types.size() == 0 && te != null) { - Element oldte = te; - te = getParent(elements, te); - if (te != null) { - exp = tail(oldte, te)+".all("+exp+")"; - types = getTypesForElement(elements, te); + if (snapshot) {// we just don't have enough information to figure out the context in a differential + if (!Utilities.noString(expression)) { + if (invariantMap.containsKey(key)) { + // it's legal - and common - for a list of elements to contain the same invariant more than once, but it's not valid if it's not always the same + ok = rule(errors, "2023-06-19", IssueType.INVALID, stack, expression.equals(invariantMap.get(key)) || "ele-1".equals(key), I18nConstants.ED_INVARIANT_EXPRESSION_CONFLICT, key, expression, invariantMap.get(key)); + } else { + invariantMap.put(key, expression); + } + if (Utilities.noString(source) || (source.equals(profileUrl))) { // no need to revalidate FHIRPath from elsewhere + try { + // we have to figure out the context, and we might be in type slicing. + String exp = expression; + Element te = element; + List types = getTypesForElement(elements, te); + while (types.size() == 0 && te != null) { + Element oldte = te; + te = getParent(elements, te); + if (te != null) { + exp = tail(oldte, te)+".all("+exp+")"; + types = getTypesForElement(elements, te); + } } - } - if (types.size() == 0) { - // we got to the root before finding anything typed - types.add(elements.get(0).getNamedChildValue("path")); - } - fpe.checkOnTypes(invariant, rootPath, types, fpe.parse(exp)); - } catch (Exception e) { - if (debug) { - e.printStackTrace(); - } - ok = rule(errors, "2023-06-19", IssueType.INVALID, stack, false, I18nConstants.ED_INVARIANT_EXPRESSION_ERROR, key, expression, e.getMessage()) && ok; - } - } + if (types.size() == 0) { + // we got to the root before finding anything typed + types.add(elements.get(0).getNamedChildValue("path")); + } + List warnings = new ArrayList<>(); + fpe.checkOnTypes(invariant, rootPath, types, fpe.parse(exp), warnings); + for (String s : warnings) { + warning(errors, "2023-07-27", IssueType.BUSINESSRULE, stack, false, key+": "+s); + } + } catch (Exception e) { + if (debug) { + e.printStackTrace(); + } + ok = rule(errors, "2023-06-19", IssueType.INVALID, stack, false, I18nConstants.ED_INVARIANT_EXPRESSION_ERROR, key, expression, e.getMessage()) && ok; + } + } + } + } else { + ok = rule(errors, "2023-07-27", IssueType.INVALID, stack, source == null || source.equals(profileUrl), I18nConstants.ED_INVARIANT_DIFF_NO_SOURCE, key, source); } } } @@ -552,6 +558,9 @@ public class StructureDefinitionValidator extends BaseValidator { } else { for (Element tr : element.getChildrenByName("type")) { String t = tr.getNamedChildValue("code"); + if (t.startsWith("http://hl7.org/fhirpath")) { + t = tr.getExtensionValue("http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type").primitiveValue(); + } if (t != null) { if (isAbstractType(t) && hasChildren(element, elements) ) { types.add(element.getNamedChildValue("path")); From 4677f319cb0d1091eca184f4f251650fde563a06 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Thu, 27 Jul 2023 20:07:36 +1000 Subject: [PATCH 6/7] Fix R4B liquid processor --- .../org/hl7/fhir/r4b/utils/FHIRLexer.java | 283 ++++++++++-------- .../hl7/fhir/r4b/utils/FHIRPathConstant.java | 20 ++ .../hl7/fhir/r4b/utils/FHIRPathEngine.java | 20 +- .../org/hl7/fhir/r4b/utils/LiquidEngine.java | 157 ++++++---- 4 files changed, 302 insertions(+), 178 deletions(-) create mode 100644 org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/FHIRPathConstant.java diff --git a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/FHIRLexer.java b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/FHIRLexer.java index ac28df765..64285888c 100644 --- a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/FHIRLexer.java +++ b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/FHIRLexer.java @@ -48,24 +48,34 @@ import org.hl7.fhir.utilities.Utilities; public class FHIRLexer { public class FHIRLexerException extends FHIRException { - public FHIRLexerException() { - super(); - } + private SourceLocation location; - public FHIRLexerException(String message, Throwable cause) { - super(message, cause); - } +// public FHIRLexerException() { +// super(); +// } +// +// public FHIRLexerException(String message, Throwable cause) { +// super(message, cause); +// } +// +// public FHIRLexerException(String message) { +// super(message); +// } +// +// public FHIRLexerException(Throwable cause) { +// super(cause); +// } - public FHIRLexerException(String message) { + public FHIRLexerException(String message, SourceLocation location) { super(message); + this.location = location; } - public FHIRLexerException(Throwable cause) { - super(cause); + public SourceLocation getLocation() { + return location; } } - private String source; private int cursor; private int currentStart; @@ -75,8 +85,10 @@ public class FHIRLexer { private SourceLocation currentStartLocation; private int id; private String name; - private boolean liquidMode; // in liquid mode, || terminates the expression and hands the parser back to the - // host + private boolean liquidMode; // in liquid mode, || terminates the expression and hands the parser back to the host + private SourceLocation commentLocation; + private boolean metadataFormat; + private boolean allowDoubleQuotes; public FHIRLexer(String source, String name) throws FHIRLexerException { this.source = source == null ? "" : source; @@ -84,35 +96,44 @@ public class FHIRLexer { currentLocation = new SourceLocation(1, 1); next(); } - public FHIRLexer(String source, int i) throws FHIRLexerException { this.source = source; this.cursor = i; currentLocation = new SourceLocation(1, 1); next(); } - + public FHIRLexer(String source, int i, boolean allowDoubleQuotes) throws FHIRLexerException { + this.source = source; + this.cursor = i; + this.allowDoubleQuotes = allowDoubleQuotes; + currentLocation = new SourceLocation(1, 1); + next(); + } + public FHIRLexer(String source, String name, boolean metadataFormat, boolean allowDoubleQuotes) throws FHIRLexerException { + this.source = source == null ? "" : source; + this.name = name == null ? "??" : name; + this.metadataFormat = metadataFormat; + this.allowDoubleQuotes = allowDoubleQuotes; + currentLocation = new SourceLocation(1, 1); + next(); + } public String getCurrent() { return current; } - public SourceLocation getCurrentLocation() { return currentLocation; } public boolean isConstant() { - return !Utilities.noString(current) && ((current.charAt(0) == '\'' || current.charAt(0) == '"') - || current.charAt(0) == '@' || current.charAt(0) == '%' || current.charAt(0) == '-' || current.charAt(0) == '+' - || (current.charAt(0) >= '0' && current.charAt(0) <= '9') || current.equals("true") || current.equals("false") - || current.equals("{}")); + return FHIRPathConstant.isFHIRPathConstant(current); } public boolean isFixedName() { - return current != null && (current.charAt(0) == '`'); + return FHIRPathConstant.isFHIRPathFixedName(current); } public boolean isStringConstant() { - return current.charAt(0) == '\'' || current.charAt(0) == '"' || current.charAt(0) == '`'; + return FHIRPathConstant.isFHIRPathStringConstant(current); } public String take() throws FHIRLexerException { @@ -124,7 +145,7 @@ public class FHIRLexer { public int takeInt() throws FHIRLexerException { String s = current; if (!Utilities.isInteger(s)) - throw error("Found " + current + " expecting an integer"); + throw error("Found "+current+" expecting an integer"); next(); return Integer.parseInt(s); } @@ -139,12 +160,10 @@ public class FHIRLexer { if (current.equals("*") || current.equals("**")) return true; - if ((current.charAt(0) >= 'A' && current.charAt(0) <= 'Z') - || (current.charAt(0) >= 'a' && current.charAt(0) <= 'z')) { - for (int i = 1; i < current.length(); i++) - if (!((current.charAt(1) >= 'A' && current.charAt(1) <= 'Z') - || (current.charAt(1) >= 'a' && current.charAt(1) <= 'z') - || (current.charAt(1) >= '0' && current.charAt(1) <= '9'))) + if ((current.charAt(0) >= 'A' && current.charAt(0) <= 'Z') || (current.charAt(0) >= 'a' && current.charAt(0) <= 'z')) { + for (int i = 1; i < current.length(); i++) + if (!( (current.charAt(1) >= 'A' && current.charAt(1) <= 'Z') || (current.charAt(1) >= 'a' && current.charAt(1) <= 'z') || + (current.charAt(1) >= '0' && current.charAt(1) <= '9'))) return false; return true; } @@ -152,11 +171,11 @@ public class FHIRLexer { } public FHIRLexerException error(String msg) { - return error(msg, currentLocation.toString()); + return error(msg, currentLocation.toString(), currentLocation); } - public FHIRLexerException error(String msg, String location) { - return new FHIRLexerException("Error @" + location + ": " + msg); + public FHIRLexerException error(String msg, String location, SourceLocation loc) { + return new FHIRLexerException("Error @"+location+": "+msg, loc); } public void next() throws FHIRLexerException { @@ -166,34 +185,30 @@ public class FHIRLexer { currentStartLocation = currentLocation; if (cursor < source.length()) { char ch = source.charAt(cursor); - if (ch == '!' || ch == '>' || ch == '<' || ch == ':' || ch == '-' || ch == '=') { + if (ch == '!' || ch == '>' || ch == '<' || ch == ':' || ch == '-' || ch == '=') { cursor++; - if (cursor < source.length() - && (source.charAt(cursor) == '=' || source.charAt(cursor) == '~' || source.charAt(cursor) == '-') - || (ch == '-' && source.charAt(cursor) == '>')) + if (cursor < source.length() && (source.charAt(cursor) == '=' || source.charAt(cursor) == '~' || source.charAt(cursor) == '-') || (ch == '-' && source.charAt(cursor) == '>')) cursor++; current = source.substring(currentStart, cursor); - } else if (ch == '.') { + } else if (ch == '.' ) { cursor++; - if (cursor < source.length() && (source.charAt(cursor) == '.')) + if (cursor < source.length() && (source.charAt(cursor) == '.')) cursor++; current = source.substring(currentStart, cursor); } else if (ch >= '0' && ch <= '9') { - cursor++; + cursor++; boolean dotted = false; - while (cursor < source.length() && ((source.charAt(cursor) >= '0' && source.charAt(cursor) <= '9') - || (source.charAt(cursor) == '.') && !dotted)) { + while (cursor < source.length() && ((source.charAt(cursor) >= '0' && source.charAt(cursor) <= '9') || (source.charAt(cursor) == '.') && !dotted)) { if (source.charAt(cursor) == '.') dotted = true; cursor++; } - if (source.charAt(cursor - 1) == '.') + if (source.charAt(cursor-1) == '.') cursor--; current = source.substring(currentStart, cursor); - } else if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { - while (cursor < source.length() && ((source.charAt(cursor) >= 'A' && source.charAt(cursor) <= 'Z') - || (source.charAt(cursor) >= 'a' && source.charAt(cursor) <= 'z') - || (source.charAt(cursor) >= '0' && source.charAt(cursor) <= '9') || source.charAt(cursor) == '_')) + } else if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { + while (cursor < source.length() && ((source.charAt(cursor) >= 'A' && source.charAt(cursor) <= 'Z') || (source.charAt(cursor) >= 'a' && source.charAt(cursor) <= 'z') || + (source.charAt(cursor) >= '0' && source.charAt(cursor) <= '9') || source.charAt(cursor) == '_')) cursor++; current = source.substring(currentStart, cursor); } else if (ch == '%') { @@ -204,19 +219,20 @@ public class FHIRLexer { cursor++; cursor++; } else - while (cursor < source.length() && ((source.charAt(cursor) >= 'A' && source.charAt(cursor) <= 'Z') - || (source.charAt(cursor) >= 'a' && source.charAt(cursor) <= 'z') - || (source.charAt(cursor) >= '0' && source.charAt(cursor) <= '9') || source.charAt(cursor) == ':' - || source.charAt(cursor) == '-')) - cursor++; + while (cursor < source.length() && ((source.charAt(cursor) >= 'A' && source.charAt(cursor) <= 'Z') || (source.charAt(cursor) >= 'a' && source.charAt(cursor) <= 'z') || + (source.charAt(cursor) >= '0' && source.charAt(cursor) <= '9') || source.charAt(cursor) == ':' || source.charAt(cursor) == '-')) + cursor++; current = source.substring(currentStart, cursor); } else if (ch == '/') { cursor++; if (cursor < source.length() && (source.charAt(cursor) == '/')) { - // this is en error - should already have been skipped - error("This shouldn't happen?"); + // we've run into metadata + cursor++; + cursor++; + current = source.substring(currentStart, cursor); + } else { + current = source.substring(currentStart, cursor); } - current = source.substring(currentStart, cursor); } else if (ch == '$') { cursor++; while (cursor < source.length() && (source.charAt(cursor) >= 'a' && source.charAt(cursor) <= 'z')) @@ -228,42 +244,42 @@ public class FHIRLexer { if (ch == '}') cursor++; current = source.substring(currentStart, cursor); - } else if (ch == '"') { + } else if (ch == '"' && allowDoubleQuotes) { cursor++; boolean escape = false; while (cursor < source.length() && (escape || source.charAt(cursor) != '"')) { if (escape) escape = false; - else + else escape = (source.charAt(cursor) == '\\'); cursor++; } if (cursor == source.length()) throw error("Unterminated string"); cursor++; - current = "\"" + source.substring(currentStart + 1, cursor - 1) + "\""; + current = "\""+source.substring(currentStart+1, cursor-1)+"\""; } else if (ch == '`') { cursor++; boolean escape = false; while (cursor < source.length() && (escape || source.charAt(cursor) != '`')) { if (escape) escape = false; - else + else escape = (source.charAt(cursor) == '\\'); cursor++; } if (cursor == source.length()) throw error("Unterminated string"); cursor++; - current = "`" + source.substring(currentStart + 1, cursor - 1) + "`"; - } else if (ch == '\'') { + current = "`"+source.substring(currentStart+1, cursor-1)+"`"; + } else if (ch == '\''){ cursor++; char ech = ch; boolean escape = false; while (cursor < source.length() && (escape || source.charAt(cursor) != ech)) { if (escape) escape = false; - else + else escape = (source.charAt(cursor) == '\\'); cursor++; } @@ -272,32 +288,32 @@ public class FHIRLexer { cursor++; current = source.substring(currentStart, cursor); if (ech == '\'') - current = "\'" + current.substring(1, current.length() - 1) + "\'"; + current = "\'"+current.substring(1, current.length() - 1)+"\'"; } else if (ch == '`') { cursor++; boolean escape = false; while (cursor < source.length() && (escape || source.charAt(cursor) != '`')) { if (escape) escape = false; - else + else escape = (source.charAt(cursor) == '\\'); cursor++; } if (cursor == source.length()) throw error("Unterminated string"); cursor++; - current = "`" + source.substring(currentStart + 1, cursor - 1) + "`"; + current = "`"+source.substring(currentStart+1, cursor-1)+"`"; } else if (ch == '|' && liquidMode) { cursor++; ch = source.charAt(cursor); if (ch == '|') cursor++; current = source.substring(currentStart, cursor); - } else if (ch == '@') { + } else if (ch == '@'){ int start = cursor; cursor++; while (cursor < source.length() && isDateChar(source.charAt(cursor), start)) - cursor++; + cursor++; current = source.substring(currentStart, cursor); } else { // if CharInSet(ch, ['.', ',', '(', ')', '=', '$']) then cursor++; @@ -308,28 +324,35 @@ public class FHIRLexer { private void skipWhitespaceAndComments() { comments.clear(); + commentLocation = null; boolean last13 = false; boolean done = false; while (cursor < source.length() && !done) { - if (cursor < source.length() - 1 && "//".equals(source.substring(cursor, cursor + 2))) { - int start = cursor + 2; - while (cursor < source.length() && !((source.charAt(cursor) == '\r') || source.charAt(cursor) == '\n')) { - cursor++; + if (cursor < source.length() -1 && "//".equals(source.substring(cursor, cursor+2)) && !isMetadataStart()) { + if (commentLocation == null) { + commentLocation = currentLocation.copy(); + } + int start = cursor+2; + while (cursor < source.length() && !((source.charAt(cursor) == '\r') || source.charAt(cursor) == '\n')) { + cursor++; } comments.add(source.substring(start, cursor).trim()); - } else if (cursor < source.length() - 1 && "/*".equals(source.substring(cursor, cursor + 2))) { - int start = cursor + 2; - while (cursor < source.length() - 1 && !"*/".equals(source.substring(cursor, cursor + 2))) { - last13 = currentLocation.checkChar(source.charAt(cursor), last13); - cursor++; + } else if (cursor < source.length() - 1 && "/*".equals(source.substring(cursor, cursor+2))) { + if (commentLocation == null) { + commentLocation = currentLocation.copy(); } - if (cursor >= source.length() - 1) { + int start = cursor+2; + while (cursor < source.length() - 1 && !"*/".equals(source.substring(cursor, cursor+2))) { + last13 = currentLocation.checkChar(source.charAt(cursor), last13); + cursor++; + } + if (cursor >= source.length() -1) { error("Unfinished comment"); } else { comments.add(source.substring(start, cursor).trim()); cursor = cursor + 2; } - } else if (Character.isWhitespace(source.charAt(cursor))) { + } else if (Utilities.isWhitespace(source.charAt(cursor))) { last13 = currentLocation.checkChar(source.charAt(cursor), last13); cursor++; } else { @@ -337,32 +360,30 @@ public class FHIRLexer { } } } - - private boolean isDateChar(char ch, int start) { - int eot = source.charAt(start + 1) == 'T' ? 10 : 20; - - return ch == '-' || ch == ':' || ch == 'T' || ch == '+' || ch == 'Z' || Character.isDigit(ch) - || (cursor - start == eot && ch == '.' && cursor < source.length() - 1 - && Character.isDigit(source.charAt(cursor + 1))); + + private boolean isMetadataStart() { + return metadataFormat && cursor < source.length() - 2 && "///".equals(source.substring(cursor, cursor+3)); + } + + private boolean isDateChar(char ch,int start) { + int eot = source.charAt(start+1) == 'T' ? 10 : 20; + + return ch == '-' || ch == ':' || ch == 'T' || ch == '+' || ch == 'Z' || Character.isDigit(ch) || (cursor-start == eot && ch == '.' && cursor < source.length()-1&& Character.isDigit(source.charAt(cursor+1))); } - public boolean isOp() { return ExpressionNode.Operation.fromCode(current) != null; } - public boolean done() { return currentStart >= source.length(); } - public int nextId() { id++; return id; } - public SourceLocation getCurrentStartLocation() { return currentStartLocation; } - + // special case use public void setCurrent(String current) { this.current = current; @@ -387,7 +408,7 @@ public class FHIRLexer { if (hasComments()) { String s = comments.get(0); comments.remove(0); - return s; + return s; } else { return null; } @@ -396,32 +417,31 @@ public class FHIRLexer { public boolean hasToken(String kw) { return !done() && kw.equals(current); } - public boolean hasToken(String... names) { - if (done()) + if (done()) return false; for (String s : names) if (s.equals(current)) return true; return false; } - + public void token(String kw) throws FHIRLexerException { - if (!kw.equals(current)) - throw error("Found \"" + current + "\" expecting \"" + kw + "\""); + if (!kw.equals(current)) + throw error("Found \""+current+"\" expecting \""+kw+"\""); next(); } - + public String readConstant(String desc) throws FHIRLexerException { if (!isStringConstant()) - throw error("Found " + current + " expecting \"[" + desc + "]\""); + throw error("Found "+current+" expecting \"["+desc+"]\""); return processConstant(take()); } public String readFixedName(String desc) throws FHIRLexerException { if (!isFixedName()) - throw error("Found " + current + " expecting \"[" + desc + "]\""); + throw error("Found "+current+" expecting \"["+desc+"]\""); return processFixedName(take()); } @@ -429,21 +449,21 @@ public class FHIRLexer { public String processConstant(String s) throws FHIRLexerException { StringBuilder b = new StringBuilder(); int i = 1; - while (i < s.length() - 1) { + while (i < s.length()-1) { char ch = s.charAt(i); if (ch == '\\') { i++; switch (s.charAt(i)) { - case 't': + case 't': b.append('\t'); break; case 'r': b.append('\r'); break; - case 'n': + case 'n': b.append('\n'); break; - case 'f': + case 'f': b.append('\f'); break; case '\'': @@ -455,20 +475,20 @@ public class FHIRLexer { case '`': b.append('`'); break; - case '\\': + case '\\': b.append('\\'); break; - case '/': + case '/': b.append('/'); break; case 'u': i++; - int uc = Integer.parseInt(s.substring(i, i + 4), 16); + int uc = Integer.parseInt(s.substring(i, i+4), 16); b.append((char) uc); i = i + 4; break; default: - throw new FHIRLexerException("Unknown character escape \\" + s.charAt(i)); + throw new FHIRLexerException("Unknown character escape \\"+s.charAt(i), currentLocation); } } else { b.append(ch); @@ -477,25 +497,25 @@ public class FHIRLexer { } return b.toString(); } - + public String processFixedName(String s) throws FHIRLexerException { StringBuilder b = new StringBuilder(); int i = 1; - while (i < s.length() - 1) { + while (i < s.length()-1) { char ch = s.charAt(i); if (ch == '\\') { i++; switch (s.charAt(i)) { - case 't': + case 't': b.append('\t'); break; case 'r': b.append('\r'); break; - case 'n': + case 'n': b.append('\n'); break; - case 'f': + case 'f': b.append('\f'); break; case '\'': @@ -504,20 +524,20 @@ public class FHIRLexer { case '"': b.append('"'); break; - case '\\': + case '\\': b.append('\\'); break; - case '/': + case '/': b.append('/'); break; case 'u': i++; - int uc = Integer.parseInt(s.substring(i, i + 4), 16); + int uc = Integer.parseInt(s.substring(i, i+4), 16); b.append((char) uc); i = i + 4; break; default: - throw new FHIRLexerException("Unknown character escape \\" + s.charAt(i)); + throw new FHIRLexerException("Unknown character escape \\"+s.charAt(i), currentLocation); } } else { b.append(ch); @@ -530,9 +550,8 @@ public class FHIRLexer { public void skipToken(String token) throws FHIRLexerException { if (getCurrent().equals(token)) next(); - + } - public String takeDottedToken() throws FHIRLexerException { StringBuilder b = new StringBuilder(); b.append(take()); @@ -542,21 +561,43 @@ public class FHIRLexer { } return b.toString(); } - + public int getCurrentStart() { return currentStart; } - public String getSource() { return source; } - public boolean isLiquidMode() { return liquidMode; } - public void setLiquidMode(boolean liquidMode) { this.liquidMode = liquidMode; } - + public SourceLocation getCommentLocation() { + return this.commentLocation; + } + public boolean isMetadataFormat() { + return metadataFormat; + } + public void setMetadataFormat(boolean metadataFormat) { + this.metadataFormat = metadataFormat; + } + public List cloneComments() { + List res = new ArrayList<>(); + res.addAll(getComments()); + return res; + } + public String tokenWithTrailingComment(String token) { + int line = getCurrentLocation().getLine(); + token(token); + if (getComments().size() > 0 && getCommentLocation().getLine() == line) { + return getFirstComment(); + } else { + return null; + } + } + public boolean isAllowDoubleQuotes() { + return allowDoubleQuotes; + } } \ No newline at end of file diff --git a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/FHIRPathConstant.java b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/FHIRPathConstant.java new file mode 100644 index 000000000..464bf7bc6 --- /dev/null +++ b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/FHIRPathConstant.java @@ -0,0 +1,20 @@ +package org.hl7.fhir.r4b.utils; + +import org.hl7.fhir.utilities.Utilities; + +public class FHIRPathConstant { + + public static boolean isFHIRPathConstant(String string) { + return !Utilities.noString(string) && ((string.charAt(0) == '\'' || string.charAt(0) == '"') || string.charAt(0) == '@' || string.charAt(0) == '%' || + string.charAt(0) == '-' || string.charAt(0) == '+' || (string.charAt(0) >= '0' && string.charAt(0) <= '9') || + string.equals("true") || string.equals("false") || string.equals("{}")); + } + + public static boolean isFHIRPathFixedName(String string) { + return string != null && (string.charAt(0) == '`'); + } + + public static boolean isFHIRPathStringConstant(String string) { + return string.charAt(0) == '\'' || string.charAt(0) == '"' || string.charAt(0) == '`'; + } +} diff --git a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/FHIRPathEngine.java b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/FHIRPathEngine.java index 73a81aacf..589b8585f 100644 --- a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/FHIRPathEngine.java +++ b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/FHIRPathEngine.java @@ -282,6 +282,7 @@ public class FHIRPathEngine { // host private boolean doNotEnforceAsSingletonRule; private boolean doNotEnforceAsCaseSensitive; + private boolean allowDoubleQuotes; // if the fhir path expressions are allowed to use constants beyond those // defined in the specification @@ -531,7 +532,7 @@ public class FHIRPathEngine { } public ExpressionNode parse(String path, String name) throws FHIRLexerException { - FHIRLexer lexer = new FHIRLexer(path, name); + FHIRLexer lexer = new FHIRLexer(path, name, false, allowDoubleQuotes); if (lexer.done()) { throw lexer.error("Path cannot be empty"); } @@ -572,7 +573,7 @@ public class FHIRPathEngine { * @throws Exception */ public ExpressionNodeWithOffset parsePartial(String path, int i) throws FHIRLexerException { - FHIRLexer lexer = new FHIRLexer(path, i); + FHIRLexer lexer = new FHIRLexer(path, i, allowDoubleQuotes); if (lexer.done()) { throw lexer.error("Path cannot be empty"); } @@ -1400,8 +1401,7 @@ public class FHIRPathEngine { private boolean checkParamCount(FHIRLexer lexer, SourceLocation location, ExpressionNode exp, int count) throws FHIRLexerException { if (exp.getParameters().size() != count) { - throw lexer.error("The function \"" + exp.getName() + "\" requires " + Integer.toString(count) + " parameters", - location.toString()); + throw lexer.error("The function \"" + exp.getName() + "\" requires " + Integer.toString(count) + " parameters"); } return true; } @@ -1410,7 +1410,7 @@ public class FHIRPathEngine { int countMax) throws FHIRLexerException { if (exp.getParameters().size() < countMin || exp.getParameters().size() > countMax) { throw lexer.error("The function \"" + exp.getName() + "\" requires between " + Integer.toString(countMin) - + " and " + Integer.toString(countMax) + " parameters", location.toString()); + + " and " + Integer.toString(countMax) + " parameters"); } return true; } @@ -6722,4 +6722,14 @@ public class FHIRPathEngine { this.liquidMode = liquidMode; } + public ProfileUtilities getProfileUtilities() { + return profileUtilities; + } + + public boolean isAllowDoubleQuotes() { + return allowDoubleQuotes; + } + public void setAllowDoubleQuotes(boolean allowDoubleQuotes) { + this.allowDoubleQuotes = allowDoubleQuotes; + } } diff --git a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/LiquidEngine.java b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/LiquidEngine.java index cb746bbfe..831edd7ef 100644 --- a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/LiquidEngine.java +++ b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/LiquidEngine.java @@ -50,6 +50,7 @@ import org.hl7.fhir.r4b.model.ValueSet; import org.hl7.fhir.r4b.utils.FHIRPathEngine.ExpressionNodeWithOffset; import org.hl7.fhir.r4b.utils.FHIRPathEngine.IEvaluationContext; import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.i18n.I18nConstants; import org.hl7.fhir.utilities.xhtml.NodeType; import org.hl7.fhir.utilities.xhtml.XhtmlNode; @@ -70,17 +71,27 @@ public class LiquidEngine implements IEvaluationContext { private class LiquidEngineContext { private Object externalContext; - private Map vars = new HashMap<>(); + private Map loopVars = new HashMap<>(); + private Map globalVars = new HashMap<>(); public LiquidEngineContext(Object externalContext) { super(); this.externalContext = externalContext; + globalVars = new HashMap<>(); + } + + public LiquidEngineContext(Object externalContext, LiquidEngineContext existing) { + super(); + this.externalContext = externalContext; + loopVars.putAll(existing.loopVars); + globalVars = existing.globalVars; } public LiquidEngineContext(LiquidEngineContext existing) { super(); externalContext = existing.externalContext; - vars.putAll(existing.vars); + loopVars.putAll(existing.loopVars); + globalVars = existing.globalVars; } } @@ -120,6 +131,7 @@ public class LiquidEngine implements IEvaluationContext { } return b.toString(); } + private abstract class LiquidNode { protected void closeUp() { @@ -148,10 +160,10 @@ public class LiquidEngine implements IEvaluationContext { } } - + private enum LiquidFilter { PREPEND; - + public static LiquidFilter fromCode(String code) { if ("prepend".equals(code)) { return PREPEND; @@ -163,15 +175,14 @@ public class LiquidEngine implements IEvaluationContext { private class LiquidExpressionNode { private LiquidFilter filter; // null at root private ExpressionNode expression; // null for some filters - public LiquidExpressionNode(LiquidFilter filter, ExpressionNode expression) { super(); this.filter = filter; this.expression = expression; } - + } - + private class LiquidStatement extends LiquidNode { private String statement; private List compiled = new ArrayList<>(); @@ -179,7 +190,7 @@ public class LiquidEngine implements IEvaluationContext { @Override public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException { if (compiled.size() == 0) { - FHIRLexer lexer = new FHIRLexer(statement, "liquid statement"); + FHIRLexer lexer = new FHIRLexer(statement, "liquid statement", false, true); lexer.setLiquidMode(true); compiled.add(new LiquidExpressionNode(null, engine.parse(lexer))); while (!lexer.done()) { @@ -188,7 +199,7 @@ public class LiquidEngine implements IEvaluationContext { String f = lexer.getCurrent(); LiquidFilter filter = LiquidFilter.fromCode(f); if (filter == null) { - lexer.error("Unknown Liquid filter '" + f + "'"); + lexer.error(engine.getWorker().formatMessage(I18nConstants.LIQUID_UNKNOWN_FILTER, f)); } lexer.next(); if (!lexer.done() && lexer.getCurrent().equals(":")) { @@ -198,21 +209,20 @@ public class LiquidEngine implements IEvaluationContext { compiled.add(new LiquidExpressionNode(filter, null)); } } else { - lexer.error("Unexpected syntax parsing liquid statement"); + lexer.error(engine.getWorker().formatMessage(I18nConstants.LIQUID_UNKNOWN_SYNTAX)); } } } - + String t = null; for (LiquidExpressionNode i : compiled) { if (i.filter == null) { // first t = stmtToString(ctxt, engine.evaluate(ctxt, resource, resource, resource, i.expression)); - } else - switch (i.filter) { - case PREPEND: - t = stmtToString(ctxt, engine.evaluate(ctxt, resource, resource, resource, i.expression)) + t; - break; - } + } else switch (i.filter) { + case PREPEND: + t = stmtToString(ctxt, engine.evaluate(ctxt, resource, resource, resource, i.expression)) + t; + break; + } } b.append(t); } @@ -221,10 +231,7 @@ public class LiquidEngine implements IEvaluationContext { StringBuilder b = new StringBuilder(); boolean first = true; for (Base i : items) { - if (first) - first = false; - else - b.append(", "); + if (first) first = false; else b.append(", "); String s = renderingSupport != null ? renderingSupport.renderForLiquid(ctxt.externalContext, i) : null; b.append(s != null ? s : engine.convertToString(i)); } @@ -312,6 +319,30 @@ public class LiquidEngine implements IEvaluationContext { } } + private class LiquidAssign extends LiquidNode { + private String varName; + private String expression; + private ExpressionNode compiled; + @Override + public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException { + if (compiled == null) { + boolean dbl = engine.isAllowDoubleQuotes(); + engine.setAllowDoubleQuotes(true); + ExpressionNodeWithOffset po = engine.parsePartial(expression, 0); + compiled = po.getNode(); + engine.setAllowDoubleQuotes(dbl); + } + List list = engine.evaluate(ctxt, resource, resource, resource, compiled); + if (list.isEmpty()) { + ctxt.globalVars.remove(varName); + } else if (list.size() == 1) { + ctxt.globalVars.put(varName, list.get(0)); + } else { + throw new Error("Assign returned a list?"); + } + } + } + private class LiquidFor extends LiquidNode { private String varName; private String condition; @@ -349,8 +380,11 @@ public class LiquidEngine implements IEvaluationContext { } if (limit >= 0 && i == limit) { break; + } + if (lctxt.globalVars.containsKey(varName)) { + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_VARIABLE_ALREADY_ASSIGNED, varName)); } - lctxt.vars.put(varName, o); + lctxt.loopVars.put(varName, o); boolean wantBreak = false; for (LiquidNode n : body) { try { @@ -379,7 +413,7 @@ public class LiquidEngine implements IEvaluationContext { } else if (cnt.startsWith("limit")) { cnt = cnt.substring(5).trim(); if (!cnt.startsWith(":")) { - throw new FHIRException("Exception evaluating " + src + ": limit is not followed by ':'"); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_COLON, src)); } cnt = cnt.substring(1).trim(); int i = 0; @@ -387,14 +421,14 @@ public class LiquidEngine implements IEvaluationContext { i++; } if (i == 0) { - throw new FHIRException("Exception evaluating " + src + ": limit is not followed by a number"); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_NUMBER, src)); } limit = Integer.parseInt(cnt.substring(0, i)); cnt = cnt.substring(i); } else if (cnt.startsWith("offset")) { cnt = cnt.substring(6).trim(); if (!cnt.startsWith(":")) { - throw new FHIRException("Exception evaluating " + src + ": limit is not followed by ':'"); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_COLON, src)); } cnt = cnt.substring(1).trim(); int i = 0; @@ -402,14 +436,14 @@ public class LiquidEngine implements IEvaluationContext { i++; } if (i == 0) { - throw new FHIRException("Exception evaluating " + src + ": limit is not followed by a number"); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_NUMBER, src)); } offset = Integer.parseInt(cnt.substring(0, i)); cnt = cnt.substring(i); } else { - throw new FHIRException("Exception evaluating " + src + ": unexpected content at " + cnt); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_UNEXPECTED, cnt)); } - } + } } } @@ -422,9 +456,9 @@ public class LiquidEngine implements IEvaluationContext { String src = includeResolver.fetchInclude(LiquidEngine.this, page); LiquidParser parser = new LiquidParser(src); LiquidDocument doc = parser.parse(page); - LiquidEngineContext nctxt = new LiquidEngineContext(ctxt.externalContext); + LiquidEngineContext nctxt = new LiquidEngineContext(ctxt.externalContext, ctxt); Tuple incl = new Tuple(); - nctxt.vars.put("include", incl); + nctxt.loopVars.put("include", incl); for (String s : params.keySet()) { incl.addProperty(s, engine.evaluate(ctxt, resource, resource, resource, params.get(s))); } @@ -481,13 +515,11 @@ public class LiquidEngine implements IEvaluationContext { cnt = "," + cnt.substring(5).trim(); while (!Utilities.noString(cnt)) { if (!cnt.startsWith(",")) { - throw new FHIRException( - "Script " + name + ": Script " + name + ": Found " + cnt.charAt(0) + " expecting ',' parsing cycle"); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_EXPECTING, name, cnt.charAt(0), ',')); } cnt = cnt.substring(1).trim(); if (!cnt.startsWith("\"")) { - throw new FHIRException( - "Script " + name + ": Script " + name + ": Found " + cnt.charAt(0) + " expecting '\"' parsing cycle"); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_EXPECTING, name, cnt.charAt(0), '"')); } cnt = cnt.substring(1); int i = 0; @@ -495,7 +527,7 @@ public class LiquidEngine implements IEvaluationContext { i++; } if (i == cnt.length()) { - throw new FHIRException("Script " + name + ": Script " + name + ": Found unterminated string parsing cycle"); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_UNTERMINATED, name)); } res.list.add(cnt.substring(0, i)); cnt = cnt.substring(i + 1).trim(); @@ -527,9 +559,10 @@ public class LiquidEngine implements IEvaluationContext { list.add(parseCycle(cnt)); else if (cnt.startsWith("include ")) list.add(parseInclude(cnt.substring(7).trim())); + else if (cnt.startsWith("assign ")) + list.add(parseAssign(cnt.substring(6).trim())); else - throw new FHIRException( - "Script " + name + ": Script " + name + ": Unknown flow control statement " + cnt); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_UNKNOWN_FLOW_STMT,name, cnt)); } else { // next2() == '{' list.add(parseStatement()); } @@ -543,8 +576,7 @@ public class LiquidEngine implements IEvaluationContext { n.closeUp(); if (terminators.length > 0) if (!isTerminator(close, terminators)) - throw new FHIRException( - "Script " + name + ": Script " + name + ": Found end of script looking for " + terminators); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_UNKNOWN_NOEND, name, terminators)); return close; } @@ -588,7 +620,7 @@ public class LiquidEngine implements IEvaluationContext { while (i < cnt.length() && !Character.isWhitespace(cnt.charAt(i))) i++; if (i == cnt.length() || i == 0) - throw new FHIRException("Script " + name + ": Error reading include: " + cnt); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_INCLUDE, name + ": Error reading include: " + cnt)); LiquidInclude res = new LiquidInclude(); res.page = cnt.substring(0, i); while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i))) @@ -598,10 +630,10 @@ public class LiquidEngine implements IEvaluationContext { while (i < cnt.length() && cnt.charAt(i) != '=') i++; if (i >= cnt.length() || j == i) - throw new FHIRException("Script " + name + ": Error reading include: " + cnt); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_INCLUDE, name, cnt)); String n = cnt.substring(j, i); if (res.params.containsKey(n)) - throw new FHIRException("Script " + name + ": Error reading include: " + cnt); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_INCLUDE, name, cnt)); i++; ExpressionNodeWithOffset t = engine.parsePartial(cnt, i); i = t.getOffset(); @@ -618,13 +650,16 @@ public class LiquidEngine implements IEvaluationContext { i++; LiquidFor res = new LiquidFor(); res.varName = cnt.substring(0, i); + if ("include".equals(res.varName)) { + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_VARIABLE_ILLEGAL, res.varName)); + } while (Character.isWhitespace(cnt.charAt(i))) i++; int j = i; while (!Character.isWhitespace(cnt.charAt(i))) i++; if (!"in".equals(cnt.substring(j, i))) - throw new FHIRException("Script " + name + ": Script " + name + ": Error reading loop: " + cnt); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_LOOP, name, cnt)); res.condition = cnt.substring(i).trim(); parseList(res.body, false, new String[] { "endloop" }); return res; @@ -636,13 +671,16 @@ public class LiquidEngine implements IEvaluationContext { i++; LiquidFor res = new LiquidFor(); res.varName = cnt.substring(0, i); + if ("include".equals(res.varName)) { + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_VARIABLE_ILLEGAL, res.varName)); + } while (Character.isWhitespace(cnt.charAt(i))) i++; int j = i; while (!Character.isWhitespace(cnt.charAt(i))) i++; if (!"in".equals(cnt.substring(j, i))) - throw new FHIRException("Script " + name + ": Script " + name + ": Error reading loop: " + cnt); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_LOOP, name, cnt)); res.condition = cnt.substring(i).trim(); String term = parseList(res.body, true, new String[] { "endfor", "else" }); if ("else".equals(term)) { @@ -651,6 +689,21 @@ public class LiquidEngine implements IEvaluationContext { return res; } + private LiquidNode parseAssign(String cnt) throws FHIRException { + int i = 0; + while (!Character.isWhitespace(cnt.charAt(i))) + i++; + LiquidAssign res = new LiquidAssign(); + res.varName = cnt.substring(0, i); + while (Character.isWhitespace(cnt.charAt(i))) + i++; + int j = i; + while (!Character.isWhitespace(cnt.charAt(i))) + i++; + res.expression = cnt.substring(i).trim(); + return res; + } + private String parseTag(char ch) throws FHIRException { grab(); grab(); @@ -659,7 +712,7 @@ public class LiquidEngine implements IEvaluationContext { b.append(grab()); } if (!(next1() == '%' && next2() == '}')) - throw new FHIRException("Script " + name + ": Unterminated Liquid statement {% " + b.toString()); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_NOTERM, name, "{% " + b.toString())); grab(); grab(); return b.toString().trim(); @@ -673,7 +726,7 @@ public class LiquidEngine implements IEvaluationContext { b.append(grab()); } if (!(next1() == '}' && next2() == '}')) - throw new FHIRException("Script " + name + ": Unterminated Liquid statement {{ " + b.toString()); + throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_NOTERM, name, "{{ " + b.toString())); grab(); grab(); LiquidStatement res = new LiquidStatement(); @@ -686,8 +739,10 @@ public class LiquidEngine implements IEvaluationContext { @Override public List resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException { LiquidEngineContext ctxt = (LiquidEngineContext) appContext; - if (ctxt.vars.containsKey(name)) - return new ArrayList(Arrays.asList(ctxt.vars.get(name))); + if (ctxt.loopVars.containsKey(name)) + return new ArrayList(Arrays.asList(ctxt.loopVars.get(name))); + if (ctxt.globalVars.containsKey(name)) + return new ArrayList(Arrays.asList(ctxt.globalVars.get(name))); if (externalHostServices == null) return new ArrayList(); return externalHostServices.resolveConstant(ctxt.externalContext, name, beforeContext); @@ -716,8 +771,7 @@ public class LiquidEngine implements IEvaluationContext { } @Override - public TypeDetails checkFunction(Object appContext, String functionName, List parameters) - throws PathEngineException { + public TypeDetails checkFunction(Object appContext, String functionName, List parameters) throws PathEngineException { if (externalHostServices == null) return null; LiquidEngineContext ctxt = (LiquidEngineContext) appContext; @@ -725,8 +779,7 @@ public class LiquidEngine implements IEvaluationContext { } @Override - public List executeFunction(Object appContext, List focus, String functionName, - List> parameters) { + public List executeFunction(Object appContext, List focus, String functionName, List> parameters) { if (externalHostServices == null) return null; LiquidEngineContext ctxt = (LiquidEngineContext) appContext; From 95d30dee241ee8078461eedb894910f4b4fb2f0b Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Thu, 27 Jul 2023 21:03:32 +1000 Subject: [PATCH 7/7] more fixing r4b test cases --- .../hl7/fhir/r4b/utils/structuremap/StructureMapUtilities.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/structuremap/StructureMapUtilities.java b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/structuremap/StructureMapUtilities.java index 76051edc1..c5151a7ee 100644 --- a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/structuremap/StructureMapUtilities.java +++ b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/structuremap/StructureMapUtilities.java @@ -713,7 +713,7 @@ public class StructureMapUtilities { } public StructureMap parse(String text, String srcName) throws FHIRException { - FHIRLexer lexer = new FHIRLexer(text, srcName); + FHIRLexer lexer = new FHIRLexer(text, srcName, true, true); if (lexer.done()) throw lexer.error("Map Input cannot be empty"); lexer.token("map");