Validate FHIRPath constraints in IGs and profiles

This commit is contained in:
Grahame Grieve 2023-06-19 22:42:02 +10:00
parent d1a785e5ad
commit 19d3952b11
11 changed files with 108 additions and 26 deletions

View File

@ -57,7 +57,7 @@ public class FmlParser extends ParserBase {
} }
public Element parse(String text) throws FHIRException { public Element parse(String text) throws FHIRException {
FHIRLexer lexer = new FHIRLexer(text, "source", true); FHIRLexer lexer = new FHIRLexer(text, "source", true, true);
if (lexer.done()) if (lexer.done())
throw lexer.error("Map Input cannot be empty"); throw lexer.error("Map Input cannot be empty");
Element result = Manager.build(context, context.fetchTypeDefinition("StructureMap")); Element result = Manager.build(context, context.fetchTypeDefinition("StructureMap"));

View File

@ -88,6 +88,7 @@ public class FHIRLexer {
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 SourceLocation commentLocation;
private boolean metadataFormat; private boolean metadataFormat;
private boolean allowDoubleQuotes;
public FHIRLexer(String source, String name) throws FHIRLexerException { public FHIRLexer(String source, String name) throws FHIRLexerException {
this.source = source == null ? "" : source; this.source = source == null ? "" : source;
@ -101,10 +102,18 @@ public class FHIRLexer {
currentLocation = new SourceLocation(1, 1); currentLocation = new SourceLocation(1, 1);
next(); next();
} }
public FHIRLexer(String source, String name, boolean metadataFormat) throws FHIRLexerException { 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.source = source == null ? "" : source;
this.name = name == null ? "??" : name; this.name = name == null ? "??" : name;
this.metadataFormat = metadataFormat; this.metadataFormat = metadataFormat;
this.allowDoubleQuotes = allowDoubleQuotes;
currentLocation = new SourceLocation(1, 1); currentLocation = new SourceLocation(1, 1);
next(); next();
} }
@ -235,7 +244,7 @@ public class FHIRLexer {
if (ch == '}') if (ch == '}')
cursor++; cursor++;
current = source.substring(currentStart, cursor); current = source.substring(currentStart, cursor);
} else if (ch == '"') { } else if (ch == '"' && allowDoubleQuotes) {
cursor++; cursor++;
boolean escape = false; boolean escape = false;
while (cursor < source.length() && (escape || source.charAt(cursor) != '"')) { while (cursor < source.length() && (escape || source.charAt(cursor) != '"')) {
@ -588,5 +597,7 @@ public class FHIRLexer {
return null; return null;
} }
} }
public boolean isAllowDoubleQuotes() {
return allowDoubleQuotes;
}
} }

View File

@ -280,6 +280,7 @@ public class FHIRPathEngine {
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 boolean doNotEnforceAsSingletonRule; private boolean doNotEnforceAsSingletonRule;
private boolean doNotEnforceAsCaseSensitive; private boolean doNotEnforceAsCaseSensitive;
private boolean allowDoubleQuotes;
// if the fhir path expressions are allowed to use constants beyond those defined in the specification // 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 // the application can implement them by providing a constant resolver
@ -511,7 +512,7 @@ public class FHIRPathEngine {
} }
public ExpressionNode parse(String path, String name) throws FHIRLexerException { 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()) { if (lexer.done()) {
throw lexer.error("Path cannot be empty"); throw lexer.error("Path cannot be empty");
} }
@ -548,7 +549,7 @@ public class FHIRPathEngine {
* @throws Exception * @throws Exception
*/ */
public ExpressionNodeWithOffset parsePartial(String path, int i) throws FHIRLexerException { 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()) { if (lexer.done()) {
throw lexer.error("Path cannot be empty"); throw lexer.error("Path cannot be empty");
} }
@ -5816,7 +5817,11 @@ public class FHIRPathEngine {
String tail = ""; String tail = "";
StructureDefinition sd = worker.fetchResource(StructureDefinition.class, url); StructureDefinition sd = worker.fetchResource(StructureDefinition.class, url);
if (sd == null) { if (sd == null) {
throw makeException(expr, I18nConstants.FHIRPATH_NO_TYPE, url, "getChildTypesByName"); if (url.startsWith(TypeDetails.FP_NS)) {
return;
} else {
throw makeException(expr, I18nConstants.FHIRPATH_UNKNOWN_TYPE, url, "getChildTypesByName");
}
} }
List<StructureDefinition> sdl = new ArrayList<StructureDefinition>(); List<StructureDefinition> sdl = new ArrayList<StructureDefinition>();
ElementDefinitionMatch m = null; ElementDefinitionMatch m = null;
@ -5826,14 +5831,14 @@ public class FHIRPathEngine {
if (m.fixedType != null) { if (m.fixedType != null) {
StructureDefinition dt = worker.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(m.fixedType, null), sd); StructureDefinition dt = worker.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(m.fixedType, null), sd);
if (dt == null) { if (dt == null) {
throw makeException(expr, I18nConstants.FHIRPATH_NO_TYPE, ProfileUtilities.sdNs(m.fixedType, null), "getChildTypesByName"); throw makeException(expr, I18nConstants.FHIRPATH_UNKNOWN_TYPE, ProfileUtilities.sdNs(m.fixedType, null), "getChildTypesByName");
} }
sdl.add(dt); sdl.add(dt);
} else } else
for (TypeRefComponent t : m.definition.getType()) { for (TypeRefComponent t : m.definition.getType()) {
StructureDefinition dt = worker.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(t.getCode(), null)); StructureDefinition dt = worker.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(t.getCode(), null));
if (dt == null) { if (dt == null) {
throw makeException(expr, I18nConstants.FHIRPATH_NO_TYPE, ProfileUtilities.sdNs(t.getCode(), null), "getChildTypesByName"); throw makeException(expr, I18nConstants.FHIRPATH_UNKNOWN_TYPE, ProfileUtilities.sdNs(t.getCode(), null), "getChildTypesByName");
} }
addTypeAndDescendents(sdl, dt, cu.allStructures()); addTypeAndDescendents(sdl, dt, cu.allStructures());
// also add any descendant types // also add any descendant types
@ -6392,4 +6397,10 @@ public class FHIRPathEngine {
return profileUtilities; return profileUtilities;
} }
public boolean isAllowDoubleQuotes() {
return allowDoubleQuotes;
}
public void setAllowDoubleQuotes(boolean allowDoubleQuotes) {
this.allowDoubleQuotes = allowDoubleQuotes;
}
} }

View File

@ -176,7 +176,7 @@ public class LiquidEngine implements IEvaluationContext {
@Override @Override
public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException { public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
if (compiled.size() == 0) { if (compiled.size() == 0) {
FHIRLexer lexer = new FHIRLexer(statement, "liquid statement"); FHIRLexer lexer = new FHIRLexer(statement, "liquid statement", false, true);
lexer.setLiquidMode(true); lexer.setLiquidMode(true);
compiled.add(new LiquidExpressionNode(null, engine.parse(lexer))); compiled.add(new LiquidExpressionNode(null, engine.parse(lexer)));
while (!lexer.done()) { while (!lexer.done()) {

View File

@ -626,7 +626,7 @@ public class StructureMapUtilities {
} }
public StructureMap parse(String text, String srcName) throws FHIRException { public StructureMap parse(String text, String srcName) throws FHIRException {
FHIRLexer lexer = new FHIRLexer(Utilities.stripBOM(text), srcName, true); FHIRLexer lexer = new FHIRLexer(Utilities.stripBOM(text), srcName, true, true);
if (lexer.done()) if (lexer.done())
throw lexer.error("Map Input cannot be empty"); throw lexer.error("Map Input cannot be empty");
StructureMap result = new StructureMap(); StructureMap result = new StructureMap();

View File

@ -3,6 +3,7 @@ package org.hl7.fhir.r5.context;
import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r5.model.PackageInformation; import org.hl7.fhir.r5.model.PackageInformation;
import org.hl7.fhir.r5.model.Parameters; import org.hl7.fhir.r5.model.Parameters;
import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.r5.model.ValueSet;
import org.hl7.fhir.r5.utils.validation.IResourceValidator; import org.hl7.fhir.r5.utils.validation.IResourceValidator;
import org.hl7.fhir.utilities.npm.BasePackageCacheManager; import org.hl7.fhir.utilities.npm.BasePackageCacheManager;
@ -75,6 +76,11 @@ public class BaseWorkerContextTests {
public String getSpecUrl() { public String getSpecUrl() {
return null; return null;
} }
@Override
public <T extends Resource> T fetchResourceRaw(Class<T> class_, String uri) {
return null;
}
}; };
baseWorkerContext.expParameters = new Parameters(); baseWorkerContext.expParameters = new Parameters();
return baseWorkerContext; return baseWorkerContext;

View File

@ -8,7 +8,7 @@ class FHIRLexerTest {
@Test @Test
@DisplayName("Test that a 'null' current value returns 'false' when FHIRLexer.isConstant() is called, and not NPE.") @DisplayName("Test that a 'null' current value returns 'false' when FHIRLexer.isConstant() is called, and not NPE.")
void getCurrent() { void getCurrent() {
FHIRLexer lexer = new FHIRLexer(null, null); FHIRLexer lexer = new FHIRLexer(null, null, false, true);
String lexerCurrent = lexer.getCurrent(); String lexerCurrent = lexer.getCurrent();
Assertions.assertNull(lexerCurrent); Assertions.assertNull(lexerCurrent);
Assertions.assertFalse(lexer.isConstant()); Assertions.assertFalse(lexer.isConstant());

View File

@ -163,9 +163,11 @@ public class SimpleHTTPClient {
} }
private void setHeaders(HttpURLConnection c) { private void setHeaders(HttpURLConnection c) {
if (headers != null) {
for (Header h : headers) { for (Header h : headers) {
c.setRequestProperty(h.getName(), h.getValue()); c.setRequestProperty(h.getName(), h.getValue());
} }
}
c.setConnectTimeout(15000); c.setConnectTimeout(15000);
c.setReadTimeout(15000); c.setReadTimeout(15000);
if (username != null) { if (username != null) {

View File

@ -466,7 +466,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
private boolean noUnicodeBiDiControlChars; private boolean noUnicodeBiDiControlChars;
private HtmlInMarkdownCheck htmlInMarkdownCheck; private HtmlInMarkdownCheck htmlInMarkdownCheck;
private boolean allowComments; private boolean allowComments;
private boolean displayWarnings; private boolean allowDoubleQuotesInFHIRPath;
private List<ImplementationGuide> igs = new ArrayList<>(); private List<ImplementationGuide> igs = new ArrayList<>();
private List<String> extensionDomains = new ArrayList<String>(); private List<String> extensionDomains = new ArrayList<String>();
@ -518,6 +518,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
fpe.setLegacyMode(true); fpe.setLegacyMode(true);
source = Source.InstanceValidator; source = Source.InstanceValidator;
fpe.setDoNotEnforceAsSingletonRule(!VersionUtilities.isR5VerOrLater(theContext.getVersion())); fpe.setDoNotEnforceAsSingletonRule(!VersionUtilities.isR5VerOrLater(theContext.getVersion()));
fpe.setAllowDoubleQuotes(allowDoubleQuotesInFHIRPath);
} }
@Override @Override
@ -2036,12 +2037,11 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
} }
} else if (ctxt.getType() == ExtensionContextType.EXTENSION) { } else if (ctxt.getType() == ExtensionContextType.EXTENSION) {
contexts.append("x:" + ctxt.getExpression()); contexts.append("x:" + ctxt.getExpression());
NodeStack estack = stack.getParent(); String ext = stack.getElement().getNamedChildValue("url");
if (estack != null && estack.getElement().fhirType().equals("Extension")) {
String ext = estack.getElement().getNamedChildValue("url");
if (ctxt.getExpression().equals(ext)) { if (ctxt.getExpression().equals(ext)) {
ok = true; ok = true;
} } else {
plist.add(ext);
} }
} else if (ctxt.getType() == ExtensionContextType.FHIRPATH) { } else if (ctxt.getType() == ExtensionContextType.FHIRPATH) {
contexts.append("p:" + ctxt.getExpression()); contexts.append("p:" + ctxt.getExpression());
@ -6490,6 +6490,14 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
} }
public boolean isAllowDoubleQuotesInFHIRPath() {
return allowDoubleQuotesInFHIRPath;
}
public void setAllowDoubleQuotesInFHIRPath(boolean allowDoubleQuotesInFHIRPath) {
this.allowDoubleQuotesInFHIRPath = allowDoubleQuotesInFHIRPath;
}
public static void setParents(Element element) { public static void setParents(Element element) {
if (element != null && !element.hasParentForValidator()) { if (element != null && !element.hasParentForValidator()) {
element.setParentForValidator(null); element.setParentForValidator(null);

View File

@ -5,7 +5,9 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashSet; import java.util.HashSet;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import org.hl7.fhir.convertors.factory.VersionConvertorFactory_10_50; import org.hl7.fhir.convertors.factory.VersionConvertorFactory_10_50;
@ -129,10 +131,10 @@ public class StructureDefinitionValidator extends BaseValidator {
boolean logical = "logical".equals(src.getNamedChildValue("kind")); boolean logical = "logical".equals(src.getNamedChildValue("kind"));
boolean constraint = "constraint".equals(src.getNamedChildValue("derivation")); boolean constraint = "constraint".equals(src.getNamedChildValue("derivation"));
for (Element differential : differentials) { for (Element differential : differentials) {
ok = validateElementList(errors, differential, stack.push(differential, -1, null, null), false, snapshots.size() > 0, sd, typeName, logical, constraint) && ok; ok = validateElementList(errors, differential, stack.push(differential, -1, null, null), false, snapshots.size() > 0, sd, typeName, logical, constraint, src.getNamedChildValue("type"), src.getNamedChildValue("url")) && ok;
} }
for (Element snapshotE : snapshots) { for (Element snapshotE : snapshots) {
ok = validateElementList(errors, snapshotE, stack.push(snapshotE, -1, null, null), true, true, sd, typeName, logical, constraint) && ok; ok = validateElementList(errors, snapshotE, stack.push(snapshotE, -1, null, null), true, true, sd, typeName, logical, constraint, src.getNamedChildValue("type"), src.getNamedChildValue("url")) && ok;
} }
// obligation profile support // obligation profile support
@ -330,18 +332,19 @@ public class StructureDefinitionValidator extends BaseValidator {
} }
} }
private boolean validateElementList(List<ValidationMessage> errors, Element elementList, NodeStack stack, boolean snapshot, boolean hasSnapshot, StructureDefinition sd, String typeName, boolean logical, boolean constraint) { private boolean validateElementList(List<ValidationMessage> errors, Element elementList, NodeStack stack, boolean snapshot, boolean hasSnapshot, StructureDefinition sd, String typeName, boolean logical, boolean constraint, String rootPath, String profileUrl) {
Map<String, String> invariantMap = new HashMap<>();
boolean ok = true; boolean ok = true;
List<Element> elements = elementList.getChildrenByName("element"); List<Element> elements = elementList.getChildrenByName("element");
int cc = 0; int cc = 0;
for (Element element : elements) { for (Element element : elements) {
ok = validateElementDefinition(errors, element, stack.push(element, cc, null, null), snapshot, hasSnapshot, sd, typeName, logical, constraint) && ok; ok = validateElementDefinition(errors, element, stack.push(element, cc, null, null), snapshot, hasSnapshot, sd, typeName, logical, constraint, invariantMap, rootPath, profileUrl) && ok;
cc++; cc++;
} }
return ok; return ok;
} }
private boolean validateElementDefinition(List<ValidationMessage> errors, Element element, NodeStack stack, boolean snapshot, boolean hasSnapshot, StructureDefinition sd, String typeName, boolean logical, boolean constraint) { private boolean validateElementDefinition(List<ValidationMessage> errors, Element element, NodeStack stack, boolean snapshot, boolean hasSnapshot, StructureDefinition sd, String typeName, boolean logical, boolean constraint, Map<String, String> invariantMap, String rootPath, String profileUrl) {
boolean ok = true; boolean ok = true;
boolean typeMustSupport = false; boolean typeMustSupport = false;
String path = element.getNamedChildValue("path"); String path = element.getNamedChildValue("path");
@ -466,6 +469,37 @@ 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 we see fixed[x] or pattern[x] applied to a repeating element, we'll give the user a hint
} }
List<Element> constraints = element.getChildrenByName("constraint");
int cc = 0;
for (Element invariant : constraints) {
ok = validateElementDefinitionInvariant(errors, invariant, stack.push(invariant, cc, null, null), invariantMap, element.getNamedChildValue("path"), rootPath, profileUrl) && ok;
cc++;
}
return ok;
}
private boolean validateElementDefinitionInvariant(List<ValidationMessage> errors, Element invariant, NodeStack stack, Map<String, String> invariantMap, String path, String rootPath, String profileUrl) {
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), I18nConstants.ED_INVARIANT_NO_EXPRESSION, key)) {
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)), 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 {
fpe.check(invariant, rootPath, path, fpe.parse(expression));
} catch (Exception e) {
ok = rule(errors, "2023-06-19", IssueType.INVALID, stack, false, I18nConstants.ED_INVARIANT_EXPRESSION_ERROR, key, expression, e.getMessage()) && ok;
}
}
}
}
return ok; return ok;
} }

View File

@ -51,11 +51,13 @@ public class ProfileValidator extends BaseValidator {
private boolean checkAggregation = false; private boolean checkAggregation = false;
private boolean checkMustSupport = false; private boolean checkMustSupport = false;
private boolean allowDoubleQuotesInFHIRPath = false;
private FHIRPathEngine fpe; private FHIRPathEngine fpe;
public ProfileValidator(IWorkerContext context, XVerExtensionManager xverManager) { public ProfileValidator(IWorkerContext context, XVerExtensionManager xverManager) {
super(context, xverManager); super(context, xverManager);
fpe = new FHIRPathEngine(context); fpe = new FHIRPathEngine(context);
fpe.setAllowDoubleQuotes(allowDoubleQuotesInFHIRPath);
} }
public boolean isCheckAggregation() { public boolean isCheckAggregation() {
@ -74,6 +76,14 @@ public class ProfileValidator extends BaseValidator {
this.checkMustSupport = checkMustSupport; this.checkMustSupport = checkMustSupport;
} }
public boolean isAllowDoubleQuotesInFHIRPath() {
return allowDoubleQuotesInFHIRPath;
}
public void setAllowDoubleQuotesInFHIRPath(boolean allowDoubleQuotesInFHIRPath) {
this.allowDoubleQuotesInFHIRPath = allowDoubleQuotesInFHIRPath;
}
protected boolean rule(List<ValidationMessage> errors, IssueType type, String path, boolean b, String msg) { protected boolean rule(List<ValidationMessage> errors, IssueType type, String path, boolean b, String msg) {
String rn = path.contains(".") ? path.substring(0, path.indexOf(".")) : path; String rn = path.contains(".") ? path.substring(0, path.indexOf(".")) : path;
return super.ruleHtml(errors, NO_RULE_DATE, type, path, b, msg, "<a href=\""+(rn.toLowerCase())+".html\">"+rn+"</a>: "+Utilities.escapeXml(msg)); return super.ruleHtml(errors, NO_RULE_DATE, type, path, b, msg, "<a href=\""+(rn.toLowerCase())+".html\">"+rn+"</a>: "+Utilities.escapeXml(msg));