From 4cdead6f8029358a3cba0b66a48316e36a9dc52f Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Wed, 20 Oct 2021 17:06:59 +1100 Subject: [PATCH] more validation for canonical URLs --- .../fhir/r5/context/BaseWorkerContext.java | 2 ++ .../org/hl7/fhir/utilities/Utilities.java | 11 +++++++++- .../fhir/utilities/i18n/I18nConstants.java | 1 + .../src/main/resources/Messages.properties | 5 +++-- .../instance/InstanceValidator.java | 22 ++++++++++++------- .../validation/instance/utils/NodeStack.java | 10 +++++++++ 6 files changed, 40 insertions(+), 11 deletions(-) diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java index f03e7685e..705d2e4ce 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java @@ -1093,6 +1093,8 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte err = TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED; } else if (it == IssueType.NOTSUPPORTED) { err = TerminologyServiceErrorClass.VALUESET_UNSUPPORTED; + } else { + err = null; } } catch (FHIRException e) { } 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 9fce7c0f0..85bca5ee5 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 @@ -68,6 +68,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank; public class Utilities { + private static final String UUID_REGEX = "[0-9a-f]{8}\\-[0-9a-f]{4}\\-[0-9a-f]{4}\\-[0-9a-f]{4}\\-[0-9a-f]{12}"; private static final String OID_REGEX = "[0-2](\\.(0|[1-9][0-9]*))+"; /** @@ -1476,7 +1477,7 @@ public class Utilities { } final static int[] illegalChars = {34, 60, 62, 124, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 58, 42, 63, 92, 47}; - + static { Arrays.sort(illegalChars); } @@ -1493,5 +1494,13 @@ public class Utilities { return cleanName.toString(); } + public static boolean isValidUUID(String uuid) { + return uuid.matches(UUID_REGEX); + } + + public static boolean isValidOID(String oid) { + return oid.matches(OID_REGEX); + } + } \ No newline at end of file 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 c2958353d..03e40d5c6 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 @@ -461,6 +461,7 @@ public class I18nConstants { public static final String TYPE_ON_FIRST_DIFFERENTIAL_ELEMENT = "type_on_first_differential_element"; public static final String TYPE_ON_FIRST_SNAPSHOT_ELEMENT_FOR__IN__FROM_ = "type_on_first_snapshot_element_for__in__from_"; public static final String TYPE_SPECIFIC_CHECKS_CANONICAL_ABSOLUTE = "TYPE_SPECIFIC_CHECKS_CANONICAL_ABSOLUTE"; + public static final String TYPE_SPECIFIC_CHECKS_CANONICAL_CONTAINED = "TYPE_SPECIFIC_CHECKS_CANONICAL_CONTAINED"; public static final String TYPE_SPECIFIC_CHECKS_DT_ATT_NO_CONTENT = "TYPE_SPECIFIC_CHECKS_DT_ATT_NO_CONTENT"; public static final String TYPE_SPECIFIC_CHECKS_DT_ATT_NO_FETCHER = "TYPE_SPECIFIC_CHECKS_DT_ATT_NO_FETCHER"; public static final String TYPE_SPECIFIC_CHECKS_DT_ATT_SIZE_CORRECT = "TYPE_SPECIFIC_CHECKS_DT_ATT_SIZE_CORRECT"; diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index 6e8259596..edcc34f0b 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -208,7 +208,7 @@ Type_Specific_Checks_DT_URI_UUID = URI values cannot start with uuid: Type_Specific_Checks_DT_URI_WS = URI values cannot have whitespace(''{0}'') Type_Specific_Checks_DT_URL_Resolve = URL value ''{0}'' does not resolve Type_Specific_Checks_DT_UUID_Strat = UUIDs must start with urn:uuid: -Type_Specific_Checks_DT_UUID_Vaid = UUIDs must be valid ({0}) +Type_Specific_Checks_DT_UUID_Vaid = UUIDs must be valid Validation_BUNDLE_Message = The first entry in a message must be a MessageHeader Validation_VAL_Content_Unknown = Unrecognised Content {0} Validation_VAL_NoType = Unknown type {0} @@ -469,7 +469,8 @@ MEASURE_M_GROUP_POP_NO_CODE = A measure group population should have a code when MEASURE_M_GROUP_STRATA_NO_CODE = A measure group stratifier should have a code when there is more than one population MEASURE_M_GROUP_STRATA_COMP_NO_CODE = A measure group stratifier component should have a code when there is more than one population MEASURE_M_LIB_UNKNOWN = The Library {0} could not be resolved, so expression validation may not be correct -TYPE_SPECIFIC_CHECKS_CANONICAL_ABSOLUTE = Canonical URLs must be absolute URLs if they are not fragment references ({0}) +TYPE_SPECIFIC_CHECKS_CANONICAL_ABSOLUTE = Canonical URLs must be absolute URLs if they are not fragment references ({0}) +TYPE_SPECIFIC_CHECKS_CANONICAL_CONTAINED = Canonical URLs in contained resources must be absolute URLs if present ({0}) MEASURE_MR_SCORE_PROHIBITED_RT = No measureScore when the type of the report is ''data-collection'' MEASURE_MR_SCORE_PROHIBITED_MS = No measureScore when the scoring of the mesage is ''cohort'' MEASURE_MR_SCORE_REQUIRED = A measureScore is required when the Measure.scoring={0} 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 2f9d0b371..c289d1a66 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 @@ -2025,23 +2025,27 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat rule(errors, IssueType.INVALID, e.line(), e.col(), path, !context.hasMaxLength() || context.getMaxLength() == 0 || e.primitiveValue().length() <= context.getMaxLength(), I18nConstants.TYPE_SPECIFIC_CHECKS_DT_PRIMITIVE_LENGTH, context.getMaxLength()); if (type.equals("oid")) { - if (rule(errors, IssueType.INVALID, e.line(), e.col(), path, url.startsWith("urn:oid:"), I18nConstants.TYPE_SPECIFIC_CHECKS_DT_OID_START)) - rule(errors, IssueType.INVALID, e.line(), e.col(), path, Utilities.isOid(url.substring(8)), I18nConstants.TYPE_SPECIFIC_CHECKS_DT_OID_VALID); + rule(errors, IssueType.INVALID, e.line(), e.col(), path, url.startsWith("urn:oid:"), I18nConstants.TYPE_SPECIFIC_CHECKS_DT_OID_START); } if (type.equals("uuid")) { rule(errors, IssueType.INVALID, e.line(), e.col(), path, url.startsWith("urn:uuid:"), I18nConstants.TYPE_SPECIFIC_CHECKS_DT_UUID_STRAT); - try { - UUID.fromString(url.substring(8)); - } catch (Exception ex) { - rule(errors, IssueType.INVALID, e.line(), e.col(), path, false, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_UUID_VAID, ex.getMessage()); - } } if (type.equals("canonical")) { rule(errors, IssueType.INVALID, e.line(), e.col(), path, url.startsWith("#") || Utilities.isAbsoluteUrl(url), I18nConstants.TYPE_SPECIFIC_CHECKS_CANONICAL_ABSOLUTE, url); } + if (url != null && url.startsWith("urn:uuid:")) { + rule(errors, IssueType.INVALID, e.line(), e.col(), path, Utilities.isValidUUID(url.substring(9)), I18nConstants.TYPE_SPECIFIC_CHECKS_DT_UUID_VAID); + } + if (url != null && url.startsWith("urn:oid:")) { + rule(errors, IssueType.INVALID, e.line(), e.col(), path, Utilities.isOid(url.substring(8)), I18nConstants.TYPE_SPECIFIC_CHECKS_DT_OID_VALID); + } + if (isCanonicalURLElement(e)) { - // for now, no validation. Need to think about authority. + // we get to here if this is a defining canonical URL (e.g. CodeSystem.url) + // the URL must be an IRI if present + rule(errors, IssueType.INVALID, e.line(), e.col(), path, Utilities.isAbsoluteUrl(url), + node.isContained() ? I18nConstants.TYPE_SPECIFIC_CHECKS_CANONICAL_CONTAINED : I18nConstants.TYPE_SPECIFIC_CHECKS_CANONICAL_ABSOLUTE, url); } else { // now, do we check the URI target? if (fetcher != null && !type.equals("uuid")) { @@ -4317,6 +4321,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat idstatus = IdStatus.OPTIONAL; break; case CONTAINED: + stack.setContained(true); idstatus = IdStatus.REQUIRED; break; case PARAMETER: @@ -5372,6 +5377,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat if ("probability is decimal implies (probability as decimal) <= 100".equals(expr)) { return "probablility.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()"; } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/NodeStack.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/NodeStack.java index e753da6d7..f1d0cc8f4 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/NodeStack.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/NodeStack.java @@ -25,6 +25,7 @@ public class NodeStack { private String workingLang; private Map ids; private boolean resetPoint = false; + private boolean contained = false; public NodeStack(IWorkerContext context) { this.context = context; @@ -101,6 +102,7 @@ public class NodeStack { res.workingLang = this.workingLang; res.element = element; res.definition = definition; + res.contained = contained; res.literalPath = getLiteralPath() + sep + element.getName(); if (count > -1) res.literalPath = res.literalPath + "[" + Integer.toString(count) + "]"; @@ -195,5 +197,13 @@ public class NodeStack { } } + public boolean isContained() { + return contained; + } + + public void setContained(boolean contained) { + this.contained = contained; + } + } \ No newline at end of file