From 816f1832d5da10e7ceb5b6346ffa3b815d0e22cb Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Wed, 27 Jul 2022 22:38:09 +1000 Subject: [PATCH 1/5] fix phinvads format --- .../java/org/hl7/fhir/convertors/misc/PhinVadsImporter.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/misc/PhinVadsImporter.java b/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/misc/PhinVadsImporter.java index 8c18746c5..4a0b26d88 100644 --- a/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/misc/PhinVadsImporter.java +++ b/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/misc/PhinVadsImporter.java @@ -1,6 +1,7 @@ package org.hl7.fhir.convertors.misc; import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.r5.formats.IParser.OutputStyle; import org.hl7.fhir.r5.formats.JsonParser; import org.hl7.fhir.r5.model.Enumerations.PublicationStatus; import org.hl7.fhir.r5.model.ValueSet; @@ -36,7 +37,7 @@ public class PhinVadsImporter extends OIDBasedValueSetImporter { System.out.println("Process " + f.getName()); ValueSet vs = importValueSet(TextFile.fileToBytes(f)); if (vs.getId() != null) { - new JsonParser().compose(new FileOutputStream(Utilities.path(dest, "ValueSet-" + vs.getId() + ".json")), vs); + new JsonParser().setOutputStyle(OutputStyle.PRETTY).compose(new FileOutputStream(Utilities.path(dest, "ValueSet-" + vs.getId() + ".json")), vs); } } catch (Exception e) { e.printStackTrace(); From 7e8cace0fb280808f52153406d13e7e347aa84e3 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Wed, 27 Jul 2022 22:38:32 +1000 Subject: [PATCH 2/5] FHIRPath fixes (string handling) --- .../org/hl7/fhir/r5/utils/FHIRPathEngine.java | 112 +++++++++++------- 1 file changed, 71 insertions(+), 41 deletions(-) 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 e35d08127..939d93026 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 @@ -2903,6 +2903,8 @@ public class FHIRPathEngine { return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Quantity); } else if (constant instanceof FHIRConstant) { return resolveConstantType(context, ((FHIRConstant) constant).getValue(), expr); + } else if (constant == null) { + return new TypeDetails(CollectionStatus.SINGLETON); } else { return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); } @@ -3140,60 +3142,60 @@ public class FHIRPathEngine { return types; } case Lower : { - checkContextString(focus, "lower", exp); + checkContextString(focus, "lower", exp, true); return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); } case Upper : { - checkContextString(focus, "upper", exp); + checkContextString(focus, "upper", exp, true); return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); } case ToChars : { - checkContextString(focus, "toChars", exp); + checkContextString(focus, "toChars", exp, true); return new TypeDetails(CollectionStatus.ORDERED, TypeDetails.FP_String); } case IndexOf : { - checkContextString(focus, "indexOf", exp); + checkContextString(focus, "indexOf", exp, true); checkParamTypes(exp, exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer); } case Substring : { - checkContextString(focus, "subString", exp); + checkContextString(focus, "subString", exp, true); checkParamTypes(exp, exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer), new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer)); return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); } case StartsWith : { - checkContextString(focus, "startsWith", exp); + checkContextString(focus, "startsWith", exp, true); checkParamTypes(exp, exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); } case EndsWith : { - checkContextString(focus, "endsWith", exp); + checkContextString(focus, "endsWith", exp, true); checkParamTypes(exp, exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); } case Matches : { - checkContextString(focus, "matches", exp); + checkContextString(focus, "matches", exp, true); checkParamTypes(exp, exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); } case MatchesFull : { - checkContextString(focus, "matches", exp); + checkContextString(focus, "matches", exp, true); checkParamTypes(exp, exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); } case ReplaceMatches : { - checkContextString(focus, "replaceMatches", exp); + checkContextString(focus, "replaceMatches", exp, true); checkParamTypes(exp, exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String), new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); } case Contains : { - checkContextString(focus, "contains", exp); + checkContextString(focus, "contains", exp, true); checkParamTypes(exp, exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); } case Replace : { - checkContextString(focus, "replace", exp); - checkParamTypes(exp, exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string"), new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); + checkContextString(focus, "replace", exp, true); + checkParamTypes(exp, exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String), new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); } case Length : { @@ -3427,37 +3429,39 @@ public class FHIRPathEngine { } - private void checkContextString(TypeDetails focus, String name, ExpressionNode expr) throws PathEngineException { - if (!focus.hasType(worker, "string") && !focus.hasType(worker, "code") && !focus.hasType(worker, "uri") && !focus.hasType(worker, "canonical") && !focus.hasType(worker, "id")) { - throw makeException(expr, I18nConstants.FHIRPATH_STRING_ONLY, name, focus.describe()); + private void checkContextString(TypeDetails focus, String name, ExpressionNode expr, boolean sing) throws PathEngineException { + if (!focus.hasNoTypes() && !focus.hasType(worker, "string") && !focus.hasType(worker, "code") && !focus.hasType(worker, "uri") && !focus.hasType(worker, "canonical") && !focus.hasType(worker, "id")) { + throw makeException(expr, sing ? I18nConstants.FHIRPATH_STRING_SING_ONLY : I18nConstants.FHIRPATH_STRING_ORD_ONLY, name, focus.describe()); } } private void checkContextPrimitive(TypeDetails focus, String name, boolean canQty, ExpressionNode expr) throws PathEngineException { - if (canQty) { - if (!focus.hasType(primitiveTypes) && !focus.hasType("Quantity")) { - throw makeException(expr, I18nConstants.FHIRPATH_PRIMITIVE_ONLY, name, focus.describe(), "Quantity, "+primitiveTypes.toString()); + if (!focus.hasNoTypes()) { + if (canQty) { + if (!focus.hasType(primitiveTypes) && !focus.hasType("Quantity")) { + throw makeException(expr, I18nConstants.FHIRPATH_PRIMITIVE_ONLY, name, focus.describe(), "Quantity, "+primitiveTypes.toString()); + } + } else if (!focus.hasType(primitiveTypes)) { + throw makeException(expr, I18nConstants.FHIRPATH_PRIMITIVE_ONLY, name, focus.describe(), primitiveTypes.toString()); } - } else if (!focus.hasType(primitiveTypes)) { - throw makeException(expr, I18nConstants.FHIRPATH_PRIMITIVE_ONLY, name, focus.describe(), primitiveTypes.toString()); } } private void checkContextNumerical(TypeDetails focus, String name, ExpressionNode expr) throws PathEngineException { - if (!focus.hasType("integer") && !focus.hasType("decimal") && !focus.hasType("Quantity")) { + if (!focus.hasNoTypes() && !focus.hasType("integer") && !focus.hasType("decimal") && !focus.hasType("Quantity")) { throw makeException(expr, I18nConstants.FHIRPATH_NUMERICAL_ONLY, name, focus.describe()); } } private void checkContextDecimal(TypeDetails focus, String name, ExpressionNode expr) throws PathEngineException { - if (!focus.hasType("decimal") && !focus.hasType("integer")) { + if (!focus.hasNoTypes() && !focus.hasType("decimal") && !focus.hasType("integer")) { throw makeException(expr, I18nConstants.FHIRPATH_DECIMAL_ONLY, name, focus.describe()); } } private void checkContextContinuous(TypeDetails focus, String name, ExpressionNode expr) throws PathEngineException { - if (!focus.hasType("decimal") && !focus.hasType("date") && !focus.hasType("dateTime") && !focus.hasType("time")) { + if (!focus.hasNoTypes() && !focus.hasType("decimal") && !focus.hasType("date") && !focus.hasType("dateTime") && !focus.hasType("time")) { throw makeException(expr, I18nConstants.FHIRPATH_CONTINUOUS_ONLY, name, focus.describe()); } } @@ -3672,7 +3676,10 @@ public class FHIRPathEngine { private List funcExp(ExecutionContext context, List focus, ExpressionNode expr) { - if (focus.size() != 1) { + if (focus.size() == 0) { + return new ArrayList(); + } + if (focus.size() > 1) { throw makeException(expr, I18nConstants.FHIRPATH_FOCUS_PLURAL, "exp", focus.size()); } Base base = focus.get(0); @@ -4218,15 +4225,19 @@ public class FHIRPathEngine { private List funcReplace(ExecutionContext context, List focus, ExpressionNode expr) throws FHIRException, PathEngineException { List result = new ArrayList(); + List tB = execute(context, focus, expr.getParameters().get(0), true); + String t = convertToString(tB); + List rB = execute(context, focus, expr.getParameters().get(1), true); + String r = convertToString(rB); - if (focus.size() == 1) { + if (focus.size() == 0 || tB.size() == 0 || rB.size() == 0) { + // + } else if (focus.size() == 1) { if (focus.get(0).hasType(FHIR_TYPES_STRING) || doImplicitStringConversion) { String f = convertToString(focus.get(0)); if (Utilities.noString(f)) { result.add(new StringType("")); } else { - String t = convertToString(execute(context, focus, expr.getParameters().get(0), true)); - String r = convertToString(execute(context, focus, expr.getParameters().get(1), true)); String n = f.replace(t, r); result.add(new StringType(n)); } @@ -4240,10 +4251,14 @@ public class FHIRPathEngine { private List funcReplaceMatches(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { List result = new ArrayList(); - String regex = convertToString(execute(context, focus, exp.getParameters().get(0), true)); - String repl = convertToString(execute(context, focus, exp.getParameters().get(1), true)); + List regexB = execute(context, focus, exp.getParameters().get(0), true); + String regex = convertToString(regexB); + List replB = execute(context, focus, exp.getParameters().get(1), true); + String repl = convertToString(replB); - if (focus.size() == 1 && !Utilities.noString(regex)) { + if (focus.size() == 0 || regexB.size() == 0 || replB.size() == 0) { + // + } else if (focus.size() == 1 && !Utilities.noString(regex)) { if (focus.get(0).hasType(FHIR_TYPES_STRING) || doImplicitStringConversion) { result.add(new StringType(convertToString(focus.get(0)).replaceAll(regex, repl)).noExtensions()); } @@ -4256,10 +4271,13 @@ public class FHIRPathEngine { private List funcEndsWith(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { List result = new ArrayList(); - String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); + List swb = execute(context, focus, exp.getParameters().get(0), true); + String sw = convertToString(swb); if (focus.size() == 0) { - result.add(new BooleanType(false).noExtensions()); + // + } else if (swb.size() == 0) { + // } else if (Utilities.noString(sw)) { result.add(new BooleanType(true).noExtensions()); } else if (focus.get(0).hasType(FHIR_TYPES_STRING) || doImplicitStringConversion) { @@ -4929,9 +4947,12 @@ public class FHIRPathEngine { private List funcMatches(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { List result = new ArrayList(); - String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); + List swb = execute(context, focus, exp.getParameters().get(0), true); + String sw = convertToString(swb); - if (focus.size() == 1 && !Utilities.noString(sw)) { + if (focus.size() == 0 || swb.size() == 0) { + // + } else if (focus.size() == 1 && !Utilities.noString(sw)) { if (focus.get(0).hasType(FHIR_TYPES_STRING) || doImplicitStringConversion) { String st = convertToString(focus.get(0)); if (Utilities.noString(st)) { @@ -4973,10 +4994,13 @@ public class FHIRPathEngine { private List funcContains(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { List result = new ArrayList(); - String sw = convertToString(execute(context, baseToList(context.thisItem), exp.getParameters().get(0), true)); + List swb = execute(context, baseToList(context.thisItem), exp.getParameters().get(0), true); + String sw = convertToString(swb); if (focus.size() != 1) { - result.add(new BooleanType(false).noExtensions()); + // + } else if (swb.size() != 1) { + // } else if (Utilities.noString(sw)) { result.add(new BooleanType(true).noExtensions()); } else if (focus.get(0).hasType(FHIR_TYPES_STRING) || doImplicitStringConversion) { @@ -5018,10 +5042,13 @@ public class FHIRPathEngine { private List funcStartsWith(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { List result = new ArrayList(); - String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); + List swb = execute(context, focus, exp.getParameters().get(0), true); + String sw = convertToString(swb); if (focus.size() == 0) { - result.add(new BooleanType(false).noExtensions()); + // no result + } else if (swb.size() == 0) { + // no result } else if (Utilities.noString(sw)) { result.add(new BooleanType(true).noExtensions()); } else if (focus.get(0).hasType(FHIR_TYPES_STRING) || doImplicitStringConversion) { @@ -5071,9 +5098,12 @@ public class FHIRPathEngine { private List funcIndexOf(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { List result = new ArrayList(); - String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); + List swb = execute(context, focus, exp.getParameters().get(0), true); + String sw = convertToString(swb); if (focus.size() == 0) { - result.add(new IntegerType(0).noExtensions()); + // no result + } else if (swb.size() == 0) { + // no result } else if (Utilities.noString(sw)) { result.add(new IntegerType(0).noExtensions()); } else if (focus.get(0).hasType(FHIR_TYPES_STRING) || doImplicitStringConversion) { From 23d8c2b5f1555baf36110d9774c07a8a9f86725f Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Wed, 27 Jul 2022 22:38:45 +1000 Subject: [PATCH 3/5] Add uuid to primitive types list --- .../src/main/java/org/hl7/fhir/r5/utils/TypesUtilities.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/TypesUtilities.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/TypesUtilities.java index 588a540f0..203612486 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/TypesUtilities.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/TypesUtilities.java @@ -170,6 +170,6 @@ public class TypesUtilities { } public static boolean isPrimitive(String code) { - return Utilities.existsInList(code, "boolean", "integer", "integer64", "string", "decimal", "uri", "url", "canonical", "base64Binary", "instant", "date", "dateTime", "time", "code", "oid", "id", "markdown", "unsignedInt", "positiveInt", "xhtml"); + return Utilities.existsInList(code, "boolean", "integer", "integer64", "string", "decimal", "uri", "url", "canonical", "base64Binary", "instant", "date", "dateTime", "time", "code", "oid", "id", "uuid", "markdown", "unsignedInt", "positiveInt", "xhtml"); } } \ No newline at end of file From 5c76af8dcdf6f10e891d50bb3583617f121b468b Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Wed, 27 Jul 2022 22:39:05 +1000 Subject: [PATCH 4/5] fhirpath fixes --- .../main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java | 3 ++- .../main/java/org/hl7/fhir/utilities/npm/PackageClient.java | 2 +- org.hl7.fhir.utilities/src/main/resources/Messages.properties | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) 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 e47a74817..a181728fd 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 @@ -173,7 +173,8 @@ public class I18nConstants { public static final String FHIRPATH_RESOLVE_DISCRIMINATOR_NO_TARGET = "FHIRPATH_RESOLVE_DISCRIMINATOR_NO_TARGET"; public static final String FHIRPATH_RIGHT_VALUE_PLURAL = "FHIRPATH_RIGHT_VALUE_PLURAL"; public static final String FHIRPATH_RIGHT_VALUE_WRONG_TYPE = "FHIRPATH_RIGHT_VALUE_WRONG_TYPE"; - public static final String FHIRPATH_STRING_ONLY = "FHIRPATH_STRING_ONLY"; + public static final String FHIRPATH_STRING_ORD_ONLY = "FHIRPATH_STRING_ORD_ONLY"; + public static final String FHIRPATH_STRING_SING_ONLY = "FHIRPATH_STRING_SING_ONLY"; public static final String FHIRPATH_UNABLE_BOOLEAN = "FHIRPATH_UNABLE_BOOLEAN"; public static final String FHIRPATH_UNKNOWN_CONSTANT = "FHIRPATH_UNKNOWN_CONSTANT"; public static final String FHIRPATH_UNKNOWN_CONTEXT = "FHIRPATH_UNKNOWN_CONTEXT"; diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/PackageClient.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/PackageClient.java index 90121542b..ed4b5298a 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/PackageClient.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/PackageClient.java @@ -244,7 +244,7 @@ public class PackageClient { public void findDependents(Set list, String id) { CommaSeparatedStringBuilder params = new CommaSeparatedStringBuilder("&"); - params.append("dependency="+id); + params.append("dependency="+id.replace("#", "|")); try { JsonArray json = fetchJsonArray(Utilities.pathURL(address, "catalog?")+params.toString()); for (JsonElement e : json) { diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index 947160694..9f9dda45e 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -557,7 +557,8 @@ FHIRPATH_WRONG_PARAM_TYPE = Error evaluating FHIRPath expression: The parameter FHIRPATH_ORDERED_ONLY = Error evaluating FHIRPath expression: The function {0} can only be used on ordered collections FHIRPATH_REFERENCE_ONLY = Error evaluating FHIRPath expression: The function {0} can only be used on ordered string, uri, canonical or Reference but found {1} FHIRPATH_CODED_ONLY = Error evaluating FHIRPath expression: The function {0} can only be used on ordered string, code, uri, Coding, CodeableConcept but found {1} -FHIRPATH_STRING_ONLY = Error evaluating FHIRPath expression: The function {0} can only be used on ordered string, uri, code, id but found {1} +FHIRPATH_STRING_ORD_ONLY = Error evaluating FHIRPath expression: The function {0} can only be used on ordered collection of string, uri, code, id but found {1} +FHIRPATH_STRING_SING_ONLY = Error evaluating FHIRPath expression: The function {0} can only be used on string, uri, code, id but found {1} FHIRPATH_NO_COLLECTION = Error evaluating FHIRPath expression: The function {0} can only be used on a singleton value but found {1} FHIRPATH_NOT_IMPLEMENTED = Error evaluating FHIRPath expression: The function {0} is not implemented FHIRPATH_PARAM_WRONG = Error evaluating FHIRPath expression: The expression type {0} is not supported for parameter {1} on function {2} From cc35c23c7d942e8fa91708fbf33233833e31cdf2 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Thu, 28 Jul 2022 08:48:33 +1000 Subject: [PATCH 5/5] fixes to validator for FHIRPath engine fixes --- .../hl7/fhir/r4b/utils/FHIRPathEngine.java | 140 ++++++++++++------ .../instance/InstanceValidator.java | 123 +++------------ .../utils/FHIRPathExpressionFixer.java | 117 +++++++++++++++ 3 files changed, 231 insertions(+), 149 deletions(-) create mode 100644 org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/FHIRPathExpressionFixer.java 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 84a3857dc..b315dc804 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 @@ -261,6 +261,7 @@ public class FHIRPathEngine { private ProfileUtilities profileUtilities; private String location; // for error messages private boolean allowPolymorphicNames; + private boolean doImplicitStringConversion; // 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 @@ -446,6 +447,14 @@ public class FHIRPathEngine { } + public boolean isDoImplicitStringConversion() { + return doImplicitStringConversion; + } + + public void setDoImplicitStringConversion(boolean doImplicitStringConversion) { + this.doImplicitStringConversion = doImplicitStringConversion; + } + // --- public API ------------------------------------------------------- /** * Parse a path for later use using execute @@ -2893,6 +2902,8 @@ public class FHIRPathEngine { return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Quantity); } else if (constant instanceof FHIRConstant) { return resolveConstantType(context, ((FHIRConstant) constant).getValue(), expr); + } else if (constant == null) { + return new TypeDetails(CollectionStatus.SINGLETON); } else { return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); } @@ -3130,60 +3141,60 @@ public class FHIRPathEngine { return types; } case Lower : { - checkContextString(focus, "lower", exp); + checkContextString(focus, "lower", exp, true); return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); } case Upper : { - checkContextString(focus, "upper", exp); + checkContextString(focus, "upper", exp, true); return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); } case ToChars : { - checkContextString(focus, "toChars", exp); + checkContextString(focus, "toChars", exp, true); return new TypeDetails(CollectionStatus.ORDERED, TypeDetails.FP_String); } case IndexOf : { - checkContextString(focus, "indexOf", exp); + checkContextString(focus, "indexOf", exp, true); checkParamTypes(exp, exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer); } case Substring : { - checkContextString(focus, "subString", exp); + checkContextString(focus, "subString", exp, true); checkParamTypes(exp, exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer), new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer)); return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); } case StartsWith : { - checkContextString(focus, "startsWith", exp); + checkContextString(focus, "startsWith", exp, true); checkParamTypes(exp, exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); } case EndsWith : { - checkContextString(focus, "endsWith", exp); + checkContextString(focus, "endsWith", exp, true); checkParamTypes(exp, exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); } case Matches : { - checkContextString(focus, "matches", exp); + checkContextString(focus, "matches", exp, true); checkParamTypes(exp, exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); } case MatchesFull : { - checkContextString(focus, "matches", exp); + checkContextString(focus, "matches", exp, true); checkParamTypes(exp, exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); } case ReplaceMatches : { - checkContextString(focus, "replaceMatches", exp); + checkContextString(focus, "replaceMatches", exp, true); checkParamTypes(exp, exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String), new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); } case Contains : { - checkContextString(focus, "contains", exp); + checkContextString(focus, "contains", exp, true); checkParamTypes(exp, exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); } case Replace : { - checkContextString(focus, "replace", exp); - checkParamTypes(exp, exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string"), new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); + checkContextString(focus, "replace", exp, true); + checkParamTypes(exp, exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String), new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); } case Length : { @@ -3417,14 +3428,15 @@ public class FHIRPathEngine { } - private void checkContextString(TypeDetails focus, String name, ExpressionNode expr) throws PathEngineException { - if (!focus.hasType(worker, "string") && !focus.hasType(worker, "code") && !focus.hasType(worker, "uri") && !focus.hasType(worker, "canonical") && !focus.hasType(worker, "id")) { - throw makeException(expr, I18nConstants.FHIRPATH_STRING_ONLY, name, focus.describe()); + private void checkContextString(TypeDetails focus, String name, ExpressionNode expr, boolean sing) throws PathEngineException { + if (!focus.hasNoTypes() && !focus.hasType(worker, "string") && !focus.hasType(worker, "code") && !focus.hasType(worker, "uri") && !focus.hasType(worker, "canonical") && !focus.hasType(worker, "id")) { + throw makeException(expr, sing ? I18nConstants.FHIRPATH_STRING_SING_ONLY : I18nConstants.FHIRPATH_STRING_ORD_ONLY, name, focus.describe()); } } private void checkContextPrimitive(TypeDetails focus, String name, boolean canQty, ExpressionNode expr) throws PathEngineException { + if (!focus.hasNoTypes()) { if (canQty) { if (!focus.hasType(primitiveTypes) && !focus.hasType("Quantity")) { throw makeException(expr, I18nConstants.FHIRPATH_PRIMITIVE_ONLY, name, focus.describe(), "Quantity, "+primitiveTypes.toString()); @@ -3433,21 +3445,22 @@ public class FHIRPathEngine { throw makeException(expr, I18nConstants.FHIRPATH_PRIMITIVE_ONLY, name, focus.describe(), primitiveTypes.toString()); } } + } private void checkContextNumerical(TypeDetails focus, String name, ExpressionNode expr) throws PathEngineException { - if (!focus.hasType("integer") && !focus.hasType("decimal") && !focus.hasType("Quantity")) { + if (!focus.hasNoTypes() && !focus.hasType("integer") && !focus.hasType("decimal") && !focus.hasType("Quantity")) { throw makeException(expr, I18nConstants.FHIRPATH_NUMERICAL_ONLY, name, focus.describe()); } } private void checkContextDecimal(TypeDetails focus, String name, ExpressionNode expr) throws PathEngineException { - if (!focus.hasType("decimal") && !focus.hasType("integer")) { + if (!focus.hasNoTypes() && !focus.hasType("decimal") && !focus.hasType("integer")) { throw makeException(expr, I18nConstants.FHIRPATH_DECIMAL_ONLY, name, focus.describe()); } } private void checkContextContinuous(TypeDetails focus, String name, ExpressionNode expr) throws PathEngineException { - if (!focus.hasType("decimal") && !focus.hasType("date") && !focus.hasType("dateTime") && !focus.hasType("time")) { + if (!focus.hasNoTypes() && !focus.hasType("decimal") && !focus.hasType("date") && !focus.hasType("dateTime") && !focus.hasType("time")) { throw makeException(expr, I18nConstants.FHIRPATH_CONTINUOUS_ONLY, name, focus.describe()); } } @@ -3662,7 +3675,10 @@ public class FHIRPathEngine { private List funcExp(ExecutionContext context, List focus, ExpressionNode expr) { - if (focus.size() != 1) { + if (focus.size() == 0) { + return new ArrayList(); + } + if (focus.size() > 1) { throw makeException(expr, I18nConstants.FHIRPATH_FOCUS_PLURAL, "exp", focus.size()); } Base base = focus.get(0); @@ -4208,17 +4224,23 @@ public class FHIRPathEngine { private List funcReplace(ExecutionContext context, List focus, ExpressionNode expr) throws FHIRException, PathEngineException { List result = new ArrayList(); + List tB = execute(context, focus, expr.getParameters().get(0), true); + String t = convertToString(tB); + List rB = execute(context, focus, expr.getParameters().get(1), true); + String r = convertToString(rB); - if (focus.size() == 1) { + if (focus.size() == 0 || tB.size() == 0 || rB.size() == 0) { + // + } else if (focus.size() == 1) { + if (focus.get(0).hasType(FHIR_TYPES_STRING) || doImplicitStringConversion) { String f = convertToString(focus.get(0)); if (Utilities.noString(f)) { result.add(new StringType("")); } else { - String t = convertToString(execute(context, focus, expr.getParameters().get(0), true)); - String r = convertToString(execute(context, focus, expr.getParameters().get(1), true)); String n = f.replace(t, r); result.add(new StringType(n)); } + } } else { throw makeException(expr, I18nConstants.FHIRPATH_NO_COLLECTION, "replace", focus.size()); } @@ -4228,11 +4250,17 @@ public class FHIRPathEngine { private List funcReplaceMatches(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { List result = new ArrayList(); - String regex = convertToString(execute(context, focus, exp.getParameters().get(0), true)); - String repl = convertToString(execute(context, focus, exp.getParameters().get(1), true)); + List regexB = execute(context, focus, exp.getParameters().get(0), true); + String regex = convertToString(regexB); + List replB = execute(context, focus, exp.getParameters().get(1), true); + String repl = convertToString(replB); - if (focus.size() == 1 && !Utilities.noString(regex)) { + if (focus.size() == 0 || regexB.size() == 0 || replB.size() == 0) { + // + } else if (focus.size() == 1 && !Utilities.noString(regex)) { + if (focus.get(0).hasType(FHIR_TYPES_STRING) || doImplicitStringConversion) { result.add(new StringType(convertToString(focus.get(0)).replaceAll(regex, repl)).noExtensions()); + } } else { result.add(new StringType(convertToString(focus.get(0))).noExtensions()); } @@ -4242,13 +4270,16 @@ public class FHIRPathEngine { private List funcEndsWith(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { List result = new ArrayList(); - String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); + List swb = execute(context, focus, exp.getParameters().get(0), true); + String sw = convertToString(swb); if (focus.size() == 0) { - result.add(new BooleanType(false).noExtensions()); + // + } else if (swb.size() == 0) { + // } else if (Utilities.noString(sw)) { result.add(new BooleanType(true).noExtensions()); - } else { + } else if (focus.get(0).hasType(FHIR_TYPES_STRING) || doImplicitStringConversion) { if (focus.size() == 1 && !Utilities.noString(sw)) { result.add(new BooleanType(convertToString(focus.get(0)).endsWith(sw)).noExtensions()); } else { @@ -4915,9 +4946,13 @@ public class FHIRPathEngine { private List funcMatches(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { List result = new ArrayList(); - String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); + List swb = execute(context, focus, exp.getParameters().get(0), true); + String sw = convertToString(swb); - if (focus.size() == 1 && !Utilities.noString(sw)) { + if (focus.size() == 0 || swb.size() == 0) { + // + } else if (focus.size() == 1 && !Utilities.noString(sw)) { + if (focus.get(0).hasType(FHIR_TYPES_STRING) || doImplicitStringConversion) { String st = convertToString(focus.get(0)); if (Utilities.noString(st)) { result.add(new BooleanType(false).noExtensions()); @@ -4927,6 +4962,7 @@ public class FHIRPathEngine { boolean ok = m.find(); result.add(new BooleanType(ok).noExtensions()); } + } } else { result.add(new BooleanType(false).noExtensions()); } @@ -4938,6 +4974,7 @@ public class FHIRPathEngine { String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); if (focus.size() == 1 && !Utilities.noString(sw)) { + if (focus.get(0).hasType(FHIR_TYPES_STRING) || doImplicitStringConversion) { String st = convertToString(focus.get(0)); if (Utilities.noString(st)) { result.add(new BooleanType(false).noExtensions()); @@ -4947,6 +4984,7 @@ public class FHIRPathEngine { boolean ok = m.matches(); result.add(new BooleanType(ok).noExtensions()); } + } } else { result.add(new BooleanType(false).noExtensions()); } @@ -4955,13 +4993,16 @@ public class FHIRPathEngine { private List funcContains(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { List result = new ArrayList(); - String sw = convertToString(execute(context, baseToList(context.thisItem), exp.getParameters().get(0), true)); + List swb = execute(context, baseToList(context.thisItem), exp.getParameters().get(0), true); + String sw = convertToString(swb); if (focus.size() != 1) { - result.add(new BooleanType(false).noExtensions()); + // + } else if (swb.size() != 1) { + // } else if (Utilities.noString(sw)) { result.add(new BooleanType(true).noExtensions()); - } else { + } else if (focus.get(0).hasType(FHIR_TYPES_STRING) || doImplicitStringConversion) { String st = convertToString(focus.get(0)); if (Utilities.noString(st)) { result.add(new BooleanType(false).noExtensions()); @@ -4980,7 +5021,7 @@ public class FHIRPathEngine { private List funcLength(ExecutionContext context, List focus, ExpressionNode exp) { List result = new ArrayList(); - if (focus.size() == 1) { + if (focus.size() == 1 && (focus.get(0).hasType(FHIR_TYPES_STRING) || doImplicitStringConversion)) { String s = convertToString(focus.get(0)); result.add(new IntegerType(s.length()).noExtensions()); } @@ -5000,13 +5041,16 @@ public class FHIRPathEngine { private List funcStartsWith(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { List result = new ArrayList(); - String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); + List swb = execute(context, focus, exp.getParameters().get(0), true); + String sw = convertToString(swb); if (focus.size() == 0) { - result.add(new BooleanType(false).noExtensions()); + // no result + } else if (swb.size() == 0) { + // no result } else if (Utilities.noString(sw)) { result.add(new BooleanType(true).noExtensions()); - } else { + } else if (focus.get(0).hasType(FHIR_TYPES_STRING) || doImplicitStringConversion) { String s = convertToString(focus.get(0)); if (s == null) { result.add(new BooleanType(false).noExtensions()); @@ -5019,7 +5063,7 @@ public class FHIRPathEngine { private List funcLower(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { List result = new ArrayList(); - if (focus.size() == 1) { + if (focus.size() == 1 && (focus.get(0).hasType(FHIR_TYPES_STRING) || doImplicitStringConversion)) { String s = convertToString(focus.get(0)); if (!Utilities.noString(s)) { result.add(new StringType(s.toLowerCase()).noExtensions()); @@ -5030,7 +5074,7 @@ public class FHIRPathEngine { private List funcUpper(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { List result = new ArrayList(); - if (focus.size() == 1) { + if (focus.size() == 1 && (focus.get(0).hasType(FHIR_TYPES_STRING) || doImplicitStringConversion)) { String s = convertToString(focus.get(0)); if (!Utilities.noString(s)) { result.add(new StringType(s.toUpperCase()).noExtensions()); @@ -5041,7 +5085,7 @@ public class FHIRPathEngine { private List funcToChars(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { List result = new ArrayList(); - if (focus.size() == 1) { + if (focus.size() == 1 && (focus.get(0).hasType(FHIR_TYPES_STRING) || doImplicitStringConversion)) { String s = convertToString(focus.get(0)); for (char c : s.toCharArray()) { result.add(new StringType(String.valueOf(c)).noExtensions()); @@ -5053,12 +5097,15 @@ public class FHIRPathEngine { private List funcIndexOf(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { List result = new ArrayList(); - String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); + List swb = execute(context, focus, exp.getParameters().get(0), true); + String sw = convertToString(swb); if (focus.size() == 0) { - result.add(new IntegerType(0).noExtensions()); + // no result + } else if (swb.size() == 0) { + // no result } else if (Utilities.noString(sw)) { result.add(new IntegerType(0).noExtensions()); - } else { + } else if (focus.get(0).hasType(FHIR_TYPES_STRING) || doImplicitStringConversion) { String s = convertToString(focus.get(0)); if (s == null) { result.add(new IntegerType(0).noExtensions()); @@ -5076,10 +5123,13 @@ public class FHIRPathEngine { int i2 = -1; if (exp.parameterCount() == 2) { List n2 = execute(context, focus, exp.getParameters().get(1), true); + if (n2.isEmpty()|| !n2.get(0).isPrimitive() || !Utilities.isInteger(n2.get(0).primitiveValue())) { + return new ArrayList(); + } i2 = Integer.parseInt(n2.get(0).primitiveValue()); } - if (focus.size() == 1) { + if (focus.size() == 1 && (focus.get(0).hasType(FHIR_TYPES_STRING) || doImplicitStringConversion)) { String sw = convertToString(focus.get(0)); String s; if (i1 < 0 || i1 >= sw.length()) { 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 14ab315a1..4188ebdd7 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 @@ -171,6 +171,7 @@ import org.hl7.fhir.validation.instance.type.StructureDefinitionValidator; import org.hl7.fhir.validation.instance.type.ValueSetValidator; import org.hl7.fhir.validation.instance.utils.ChildIterator; import org.hl7.fhir.validation.instance.utils.ElementInfo; +import org.hl7.fhir.validation.instance.utils.FHIRPathExpressionFixer; import org.hl7.fhir.validation.instance.utils.IndexedElement; import org.hl7.fhir.validation.instance.utils.NodeStack; import org.hl7.fhir.validation.instance.utils.ResolvedReference; @@ -210,8 +211,8 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat public CanonicalResourceLookupResult(String error) { this.error = error; } - } + private static final String EXECUTED_CONSTRAINT_LIST = "validator.executed.invariant.list"; private static final String EXECUTION_ID = "validator.execution.id"; private static final String HTML_FRAGMENT_REGEX = "[a-zA-Z]\\w*(((\\s+)(\\S)*)*)"; @@ -3461,7 +3462,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } TypedElementDefinition ted = null; - String fp = fixExpr(discriminator, null); + String fp = FHIRPathExpressionFixer.fixExpr(discriminator, null); ExpressionNode expr = null; try { expr = fpe.parse(fp); @@ -4068,7 +4069,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } try { - n = fpe.parse(fixExpr(expression.toString(), null)); + n = fpe.parse(FHIRPathExpressionFixer.fixExpr(expression.toString(), null)); } catch (FHIRLexerException e) { if (STACK_TRACE) e.printStackTrace(); throw new FHIRException(context.formatMessage(I18nConstants.PROBLEM_PROCESSING_EXPRESSION__IN_PROFILE__PATH__, expression, profile.getUrl(), path, e.getMessage())); @@ -5590,7 +5591,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } List invErrors = null; // We key based on inv.expression rather than inv.key because expressions can change in derived profiles and aren't guaranteed to be consistent across profiles. - String key = fixExpr(inv.getExpression(), inv.getKey()); + String key = FHIRPathExpressionFixer.fixExpr(inv.getExpression(), inv.getKey()); if (!invMap.keySet().contains(key)) { invErrors = new ArrayList(); invMap.put(key, invErrors); @@ -5643,7 +5644,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat if (n == null) { long t = System.nanoTime(); try { - n = fpe.parse(fixExpr(inv.getExpression(), inv.getKey())); + n = fpe.parse(FHIRPathExpressionFixer.fixExpr(inv.getExpression(), inv.getKey())); } catch (FHIRLexerException e) { rule(errors, IssueType.INVARIANT, element.line(), element.col(), path, false, I18nConstants.PROBLEM_PROCESSING_EXPRESSION__IN_PROFILE__PATH__, inv.getExpression(), profile.getUrl(), path, e.getMessage()); return; @@ -5665,25 +5666,26 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat ex.printStackTrace(); } if (!ok) { - if (!Utilities.noString(msg)) { - msg = "'" + inv.getHuman()+"' (" + msg + ")"; - } else if (wantInvariantInMessage) { - msg = "'" + inv.getHuman()+"' [" + n.toString() + "]"; - } else { - msg = context.formatMessage(I18nConstants.INV_FAILED, "'" + inv.getHuman()+"'"); + if (wantInvariantInMessage) { + msg = msg + " (inv = " + n.toString() + ")"; } + if (!Utilities.noString(msg)) { + msg = msg + " (log: " + msg + ")"; + } + msg = context.formatMessage(I18nConstants.INV_FAILED, inv.getKey() + ": '" + inv.getHuman()+"'")+msg; + if (inv.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice") && ToolingExtensions.readBooleanExtension(inv, "http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice")) { if (bpWarnings == BestPracticeWarningLevel.Hint) - hint(errors, IssueType.INVARIANT, element.line(), element.col(), path, ok, inv.getKey() + ": " + msg); + hint(errors, IssueType.INVARIANT, element.line(), element.col(), path, ok, msg); else if (bpWarnings == BestPracticeWarningLevel.Warning) - warning(errors, IssueType.INVARIANT, element.line(), element.col(), path, ok, inv.getKey() + ": '" + inv.getHuman()+"' " + msg); + warning(errors, IssueType.INVARIANT, element.line(), element.col(), path, ok, msg); else if (bpWarnings == BestPracticeWarningLevel.Error) - rule(errors, IssueType.INVARIANT, element.line(), element.col(), path, ok, inv.getKey() + ": '" + inv.getHuman()+"' " + msg); + rule(errors, IssueType.INVARIANT, element.line(), element.col(), path, ok, msg); } else if (inv.getSeverity() == ConstraintSeverity.ERROR) { - rule(errors, IssueType.INVARIANT, element.line(), element.col(), path, ok, inv.getKey() + ": '" + inv.getHuman()+"' " + msg); + rule(errors, IssueType.INVARIANT, element.line(), element.col(), path, ok, msg); } else if (inv.getSeverity() == ConstraintSeverity.WARNING) { - warning(errors, IssueType.INVARIANT, element.line(), element.col(), path, ok, inv.getKey() + ": '" + inv.getHuman()+"' " + msg); + warning(errors, IssueType.INVARIANT, element.line(), element.col(), path, ok, msg); } } } @@ -5838,7 +5840,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat try { ExpressionNode n = (ExpressionNode) inv.getUserData("validator.expression.cache"); if (n == null) { - n = fpe.parse(fixExpr(inv.getExpression(), inv.getKey())); + n = fpe.parse(FHIRPathExpressionFixer.fixExpr(inv.getExpression(), inv.getKey())); inv.setUserData("validator.expression.cache", n); } fpe.check(null, sd.getKind() == StructureDefinitionKind.RESOURCE ? sd.getType() : "DomainResource", ed.getPath(), n); @@ -5852,93 +5854,6 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } } - private String fixExpr(String expr, String key) { - // this is a hack work around for past publication of wrong FHIRPath expressions - // R4 - // waiting for 4.0.2 - //TODO is this expression below correct? @grahamegrieve - if ("probability is decimal implies (probability as decimal) <= 100".equals(expr)) { - return "probability.empty() or ((probability is decimal) implies ((probability as decimal) <= 100))"; - } - if ("enableWhen.count() > 2 implies enableBehavior.exists()".equals(expr)) { - return "enableWhen.count() >= 2 implies enableBehavior.exists()"; - } - if ("txt-2".equals(key)) { - return "htmlChecks2()"; - } - - // clarification in FHIRPath spec - if (expr.equals("name.matches('[A-Z]([A-Za-z0-9_]){0,254}')")) { - return "name.matches('^[A-Z]([A-Za-z0-9_]){0,254}$')"; - } - if ("eld-19".equals(key)) { - return "path.matches('^[^\\\\s\\\\.,:;\\\\\\'\"\\\\/|?!@#$%&*()\\\\[\\\\]{}]{1,64}(\\\\.[^\\\\s\\\\.,:;\\\\\\'\"\\\\/|?!@#$%&*()\\\\[\\\\]{}]{1,64}(\\\\[x\\\\])?(\\\\:[^\\\\s\\\\.]+)?)*$')"; - } - if ("eld-20".equals(key)) { - return "path.matches('^[A-Za-z][A-Za-z0-9]*(\\\\.[a-z][A-Za-z0-9]*(\\\\[x])?)*$')"; - } - - // handled in 4.0.1 - if ("(component.empty() and hasMember.empty()) implies (dataAbsentReason or value)".equals(expr)) { - return "(component.empty() and hasMember.empty()) implies (dataAbsentReason.exists() or value.exists())"; - } - if ("isModifier implies isModifierReason.exists()".equals(expr)) { - return "(isModifier.exists() and isModifier) implies isModifierReason.exists()"; - } - if ("(%resource.kind = 'logical' or element.first().path.startsWith(%resource.type)) and (element.tail().not() or element.tail().all(path.startsWith(%resource.differential.element.first().path.replaceMatches('\\\\..*','')&'.')))".equals(expr)) { - return "(%resource.kind = 'logical' or element.first().path.startsWith(%resource.type)) and (element.tail().empty() or element.tail().all(path.startsWith(%resource.differential.element.first().path.replaceMatches('\\\\..*','')&'.')))"; - } - if ("differential.element.all(id) and differential.element.id.trace('ids').isDistinct()".equals(expr)) { - return "differential.element.all(id.exists()) and differential.element.id.trace('ids').isDistinct()"; - } - if ("snapshot.element.all(id) and snapshot.element.id.trace('ids').isDistinct()".equals(expr)) { - return "snapshot.element.all(id.exists()) and snapshot.element.id.trace('ids').isDistinct()"; - } - - // R3 - if ("(code or value.empty()) and (system.empty() or system = 'urn:iso:std:iso:4217')".equals(expr)) { - return "(code.exists() or value.empty()) and (system.empty() or system = 'urn:iso:std:iso:4217')"; - } - if ("value.empty() or code!=component.code".equals(expr)) { - return "value.empty() or (code in component.code).not()"; - } - if ("(code or value.empty()) and (system.empty() or system = %ucum) and (value.empty() or value > 0)".equals(expr)) { - return "(code.exists() or value.empty()) and (system.empty() or system = %ucum) and (value.empty() or value > 0)"; - } - if ("element.all(definition and min and max)".equals(expr)) { - return "element.all(definition.exists() and min.exists() and max.exists())"; - } - if ("telecom or endpoint".equals(expr)) { - return "telecom.exists() or endpoint.exists()"; - } - if ("(code or value.empty()) and (system.empty() or system = %ucum) and (value.empty() or value > 0)".equals(expr)) { - return "(code.exists() or value.empty()) and (system.empty() or system = %ucum) and (value.empty() or value > 0)"; - } - if ("searchType implies type = 'string'".equals(expr)) { - return "searchType.exists() implies type = 'string'"; - } - if ("abatement.empty() or (abatement as boolean).not() or clinicalStatus='resolved' or clinicalStatus='remission' or clinicalStatus='inactive'".equals(expr)) { - return "abatement.empty() or (abatement is boolean).not() or (abatement as boolean).not() or (clinicalStatus = 'resolved') or (clinicalStatus = 'remission') or (clinicalStatus = 'inactive')"; - } - if ("(component.empty() and related.empty()) implies (dataAbsentReason or value)".equals(expr)) { - return "(component.empty() and related.empty()) implies (dataAbsentReason.exists() or value.exists())"; - } - if ("reference.startsWith('#').not() or (reference.substring(1).trace('url') in %rootResource.contained.id.trace('ids'))".equals(expr)) { - return "(reference = '#') or reference.startsWith('#').not() or (reference.substring(1).trace('url') in %rootResource.contained.id.trace('ids'))"; - } - if ("reference.startsWith('#').not() or (reference.substring(1).trace('url') in %resource.contained.id.trace('ids'))".equals(expr)) { - return "(reference = '#') or reference.startsWith('#').not() or (reference.substring(1).trace('url') in %rootResource.contained.id.trace('ids'))"; - } - if ("probability is decimal implies probability.as(decimal) <= 100".equals(expr)) { - if (key.equals("ras-2")) { - return "probability.empty() or (probability is decimal implies probability.as(decimal) <= 100)"; - } - } - if ("".equals(expr)) { - return ""; - } - return expr; - } public IEvaluationContext getExternalHostServices() { return externalHostServices; diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/FHIRPathExpressionFixer.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/FHIRPathExpressionFixer.java new file mode 100644 index 000000000..fad76a3de --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/FHIRPathExpressionFixer.java @@ -0,0 +1,117 @@ +package org.hl7.fhir.validation.instance.utils; + +import org.hl7.fhir.utilities.Utilities; + +public class FHIRPathExpressionFixer { + + + public static String fixExpr(String expr, String key) { + // this is a hack work around for past publication of wrong FHIRPath expressions + // R4 + // waiting for 4.0.2 + //TODO is this expression below correct? @grahamegrieve + if ("probability is decimal implies (probability as decimal) <= 100".equals(expr)) { + return "probability.empty() or ((probability is decimal) implies ((probability as decimal) <= 100))"; + } + if ("enableWhen.count() > 2 implies enableBehavior.exists()".equals(expr)) { + return "enableWhen.count() >= 2 implies enableBehavior.exists()"; + } + if ("txt-2".equals(key)) { + return "htmlChecks2()"; + } + + // fixes to string functions in FHIRPath + // ref-1 + if (expr.equals("reference.startsWith('#').not() or (reference.substring(1).trace('url') in %rootResource.contained.id.trace('ids')) or (reference='#' and %rootResource!=%resource)")) { // R5 + return "reference.exists() implies ("+expr+")"; + } + if (expr.equals("reference.startsWith('#').not() or (reference.substring(1).trace('url') in %rootResource.contained.id.trace('ids'))")) { // R4/R4B + return "reference.exists() implies (reference = '#' or ("+expr+"))"; + } + if (expr.equals("reference.startsWith('#').not() or (reference.substring(1).trace('url') in %resource.contained.id.trace('ids'))")) { // STU3 + return "reference.exists() implies (reference = '#' or (reference.startsWith('#').not() or (reference.substring(1).trace('url') in %rootResource.contained.id.trace('ids'))))"; + } + // bld-8 + if (expr.equals("fullUrl.contains('/_history/').not()")) { // R4 + return "fullUrl.exists() implies fullUrl.contains('/_history/').not()"; + } + if (expr.equals("name.matches('[A-Z]([A-Za-z0-9_]){0,254}')")) { + return "name.exists() implies name.matches('^[A-Z]([A-Za-z0-9_]){0,254}$')"; + } + // canonical + if (expr.equals("name.matches('[A-Z]([A-Za-z0-9_]){0,254}')")) { + return ("name.exists() implies name.matches('[A-Z]([A-Za-z0-9_]){0,254}')"); + } + + + // clarification in FHIRPath spec + if ("eld-19".equals(key)) { + return "path.matches('^[^\\\\s\\\\.,:;\\\\\\'\"\\\\/|?!@#$%&*()\\\\[\\\\]{}]{1,64}(\\\\.[^\\\\s\\\\.,:;\\\\\\'\"\\\\/|?!@#$%&*()\\\\[\\\\]{}]{1,64}(\\\\[x\\\\])?(\\\\:[^\\\\s\\\\.]+)?)*$')"; + } + if ("eld-20".equals(key)) { + return "path.matches('^[A-Za-z][A-Za-z0-9]*(\\\\.[a-z][A-Za-z0-9]*(\\\\[x])?)*$')"; + } + + // handled in 4.0.1 + if ("(component.empty() and hasMember.empty()) implies (dataAbsentReason or value)".equals(expr)) { + return "(component.empty() and hasMember.empty()) implies (dataAbsentReason.exists() or value.exists())"; + } + if ("isModifier implies isModifierReason.exists()".equals(expr)) { + return "(isModifier.exists() and isModifier) implies isModifierReason.exists()"; + } + if ("(%resource.kind = 'logical' or element.first().path.startsWith(%resource.type)) and (element.tail().not() or element.tail().all(path.startsWith(%resource.differential.element.first().path.replaceMatches('\\\\..*','')&'.')))".equals(expr)) { + return "(%resource.kind = 'logical' or element.first().path.startsWith(%resource.type)) and (element.tail().empty() or element.tail().all(path.startsWith(%resource.differential.element.first().path.replaceMatches('\\\\..*','')&'.')))"; + } + if ("differential.element.all(id) and differential.element.id.trace('ids').isDistinct()".equals(expr)) { + return "differential.element.all(id.exists()) and differential.element.id.trace('ids').isDistinct()"; + } + if ("snapshot.element.all(id) and snapshot.element.id.trace('ids').isDistinct()".equals(expr)) { + return "snapshot.element.all(id.exists()) and snapshot.element.id.trace('ids').isDistinct()"; + } + + // R3 + if ("(code or value.empty()) and (system.empty() or system = 'urn:iso:std:iso:4217')".equals(expr)) { + return "(code.exists() or value.empty()) and (system.empty() or system = 'urn:iso:std:iso:4217')"; + } + if ("value.empty() or code!=component.code".equals(expr)) { + return "value.empty() or (code in component.code).not()"; + } + if ("(code or value.empty()) and (system.empty() or system = %ucum) and (value.empty() or value > 0)".equals(expr)) { + return "(code.exists() or value.empty()) and (system.empty() or system = %ucum) and (value.empty() or value > 0)"; + } + if ("element.all(definition and min and max)".equals(expr)) { + return "element.all(definition.exists() and min.exists() and max.exists())"; + } + if ("telecom or endpoint".equals(expr)) { + return "telecom.exists() or endpoint.exists()"; + } + if ("(code or value.empty()) and (system.empty() or system = %ucum) and (value.empty() or value > 0)".equals(expr)) { + return "(code.exists() or value.empty()) and (system.empty() or system = %ucum) and (value.empty() or value > 0)"; + } + if ("searchType implies type = 'string'".equals(expr)) { + return "searchType.exists() implies type = 'string'"; + } + if ("abatement.empty() or (abatement as boolean).not() or clinicalStatus='resolved' or clinicalStatus='remission' or clinicalStatus='inactive'".equals(expr)) { + return "abatement.empty() or (abatement is boolean).not() or (abatement as boolean).not() or (clinicalStatus = 'resolved') or (clinicalStatus = 'remission') or (clinicalStatus = 'inactive')"; + } + if ("(component.empty() and related.empty()) implies (dataAbsentReason or value)".equals(expr)) { + return "(component.empty() and related.empty()) implies (dataAbsentReason.exists() or value.exists())"; + } + if ("reference.startsWith('#').not() or (reference.substring(1).trace('url') in %rootResource.contained.id.trace('ids'))".equals(expr)) { + return "(reference = '#') or reference.startsWith('#').not() or (reference.substring(1).trace('url') in %rootResource.contained.id.trace('ids'))"; + } + if ("reference.startsWith('#').not() or (reference.substring(1).trace('url') in %resource.contained.id.trace('ids'))".equals(expr)) { + return "(reference = '#') or reference.startsWith('#').not() or (reference.substring(1).trace('url') in %rootResource.contained.id.trace('ids'))"; + } + if ("probability is decimal implies probability.as(decimal) <= 100".equals(expr)) { + if (key.equals("ras-2")) { + return "probability.empty() or (probability is decimal implies probability.as(decimal) <= 100)"; + } + } + if ("".equals(expr)) { + return ""; + } + return expr; + } + +}