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 1d5359d76..4d9da697d 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 @@ -341,6 +341,8 @@ public class I18nConstants { public static final String RESOURCE_TYPE_MISMATCH_FOR___ = "Resource_type_mismatch_for___"; public static final String SAME_ID_ON_MULTIPLE_ELEMENTS__IN_ = "Same_id_on_multiple_elements__in_"; public static final String SD_MUST_HAVE_DERIVATION = "SD_MUST_HAVE_DERIVATION"; + public static final String SD_NESTED_MUST_SUPPORT_DIFF = "SD_NESTED_MUST_SUPPORT_DIFF"; + public static final String SD_NESTED_MUST_SUPPORT_SNAPSHOT = "SD_NESTED_MUST_SUPPORT_SNAPSHOT"; public static final String SEARCHPARAMETER_BASE_WRONG = "SEARCHPARAMETER_BASE_WRONG"; public static final String SEARCHPARAMETER_EXP_WRONG = "SEARCHPARAMETER_EXP_WRONG"; public static final String SEARCHPARAMETER_NOTFOUND = "SEARCHPARAMETER_NOTFOUND"; diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index 562e34887..18bee95a6 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -608,4 +608,7 @@ REFERENCE_REF_SUSPICIOUS = The syntax of the reference ''{0}'' looks incorrect, TYPE_SPECIFIC_CHECKS_DT_QTY_NO_ANNOTATIONS = UCUM Codes that contain human readable annotations like {0} can be misleading. Best Practice is not to use annotations in the UCUM code, and rather to make sure that Quantity.unit is correctly human readable XHTML_XHTML_ELEMENT_ILLEGAL_IN_PARA = Illegal element name inside in a paragraph in the XHTML (''{0}'') UNSUPPORTED_IDENTIFIER_PATTERN_PROPERTY_NOT_SUPPORTED_FOR_DISCRIMINATOR_FOR_SLICE = Unsupported property {3} on type {2} for pattern for discriminator({0}) for slice {1} -UNSUPPORTED_IDENTIFIER_PATTERN_NO_PROPERTY_NOT_SUPPORTED_FOR_DISCRIMINATOR_FOR_SLICE = Unsupported: no properties with values found on type {2} for pattern for discriminator({0}) for slice {1} \ No newline at end of file +UNSUPPORTED_IDENTIFIER_PATTERN_NO_PROPERTY_NOT_SUPPORTED_FOR_DISCRIMINATOR_FOR_SLICE = Unsupported: no properties with values found on type {2} for pattern for discriminator({0}) for slice {1} +SD_NESTED_MUST_SUPPORT_DIFF = The element {0} has types/profiles/targets that are marked as must support, but the element itself is not marked as must-support. The inner must-supports will be ignored unless the element inherits must-support = true +SD_NESTED_MUST_SUPPORT_SNAPSHOT = The element {0} has types/profiles/targets that are marked as must support, but the element itself is not marked as must-support + \ No newline at end of file diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java index 9e1bd5dd3..c36db51d3 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java @@ -296,9 +296,10 @@ public class BaseValidator { * Set this parameter to false if the validation does not pass * @return Returns thePass (in other words, returns true if the rule did not fail validation) */ - protected boolean hint(List errors, IssueType type, String path, boolean thePass, String msg) { + protected boolean hint(List errors, IssueType type, String path, boolean thePass, String theMessage, Object... theMessageArguments) { if (!thePass) { - addValidationMessage(errors, type, -1, -1, path, msg, IssueSeverity.INFORMATION, null); + String message = context.formatMessage(theMessage, theMessageArguments); + addValidationMessage(errors, type, -1, -1, path, message, IssueSeverity.INFORMATION, null); } return thePass; } 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 02aee0df3..0c7ab8330 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 @@ -483,7 +483,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat private boolean isKnownExtension(String url) { // Added structuredefinition-expression and following extensions explicitly because they weren't defined in the version of the spec they need to be used with - if ((allowExamples && (url.contains("example.org") || url.contains("acme.com"))) || url.contains("nema.org") || url.startsWith("http://hl7.org/fhir/tools/StructureDefinition/") || url.equals("http://hl7.org/fhir/StructureDefinition/structuredefinition-expression") || url.equals(VersionConvertorConstants.IG_DEPENDSON_PACKAGE_EXTENSION)) + if ((allowExamples && (url.contains("example.org") || url.contains("acme.com"))) || url.contains("nema.org") || url.startsWith("http://hl7.org/fhir/tools/StructureDefinition/") || url.equals("http://hl7.org/fhir/StructureDefinition/structuredefinition-expression")) return true; for (String s : extensionDomains) if (url.startsWith(s)) @@ -1521,6 +1521,8 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat if (extensionUrl.equals(profile.getUrl())) { rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", hasExtensionSlice(profile, url), I18nConstants.EXTENSION_EXT_SUBEXTENSION_INVALID, url, profile.getUrl()); } + } else if (SpecialExtensions.isKnownExtension(url)) { + ex = SpecialExtensions.getDefinition(url); } else if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, allowUnknownExtension(url), I18nConstants.EXTENSION_EXT_UNKNOWN_NOTHERE, url)) { hint(errors, IssueType.STRUCTURE, element.line(), element.col(), path, isKnownExtension(url), I18nConstants.EXTENSION_EXT_UNKNOWN, url); } @@ -1556,6 +1558,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat return ex; } + private boolean hasExtensionSlice(StructureDefinition profile, String sliceName) { for (ElementDefinition ed : profile.getSnapshot().getElement()) { if (ed.getPath().equals("Extension.extension.url") && ed.hasFixed() && sliceName.equals(ed.getFixed().primitiveValue())) { @@ -1933,7 +1936,8 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat if (fetcher != null) { boolean found; try { - found = isDefinitionURL(url) || (allowExamples && (url.contains("example.org") || url.contains("acme.com")) || url.contains("acme.org")) || (url.startsWith("http://hl7.org/fhir/tools")) || fetcher.resolveURL(appContext, path, url); + found = isDefinitionURL(url) || (allowExamples && (url.contains("example.org") || url.contains("acme.com")) || url.contains("acme.org")) || (url.startsWith("http://hl7.org/fhir/tools")) || fetcher.resolveURL(appContext, path, url) || + SpecialExtensions.isKnownExtension(url); } catch (IOException e1) { found = false; } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/SpecialExtensions.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/SpecialExtensions.java new file mode 100644 index 000000000..a51f75ef1 --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/SpecialExtensions.java @@ -0,0 +1,39 @@ +package org.hl7.fhir.validation.instance; + +import java.io.IOException; +import java.io.InputStream; + +import org.hl7.fhir.convertors.VersionConvertorConstants; +import org.hl7.fhir.exceptions.FHIRFormatError; +import org.hl7.fhir.r5.formats.JsonParser; +import org.hl7.fhir.r5.formats.XmlParser; +import org.hl7.fhir.r5.model.StructureDefinition; +import org.hl7.fhir.utilities.Utilities; + +public class SpecialExtensions { + + // copied from R5 spec + private static final String MUST_SUPPORT_SOURCE = "{\"resourceType\" : \"StructureDefinition\",\"id\" : \"elementdefinition-type-must-support\",\"extension\" : [{\"url\" : \"http://hl7.org/fhir/StructureDefinition/structuredefinition-wg\",\"valueCode\" : \"fhir\"},{\"url\" : \"http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm\",\"valueInteger\" : 1}],\"url\" : \"http://hl7.org/fhir/StructureDefinition/elementdefinition-type-must-support\",\"version\" : \"4.5.0\",\"name\" : \"type-must-support\",\"status\" : \"draft\",\"date\" : \"2015-02-28\",\"publisher\" : \"Health Level Seven, Inc. - FHIR Core WG\",\"contact\" : [{\"telecom\" : [{\"system\" : \"url\",\"value\" : \"http://hl7.org/special/committees/FHIR\"}]}],\"description\" : \"If true indicates that the specified type, profile or targetProfile must be supported by implementations.\",\"fhirVersion\" : \"4.5.0\",\"mapping\" : [{\"identity\" : \"rim\",\"uri\" : \"http://hl7.org/v3\",\"name\" : \"RIM Mapping\"}],\"kind\" : \"complex-type\",\"abstract\" : false,\"context\" : [{\"type\" : \"element\",\"expression\" : \"ElementDefinition.type\"},{\"type\" : \"element\",\"expression\" : \"ElementDefinition.type.profile\"},{\"type\" : \"element\",\"expression\" : \"ElementDefinition.type.targetProfile\"}],\"type\" : \"Extension\",\"baseDefinition\" : \"http://hl7.org/fhir/StructureDefinition/Extension\",\"derivation\" : \"constraint\",\"snapshot\" : {\"element\" : [{\"id\" : \"Extension\",\"path\" : \"Extension\",\"short\" : \"The specified type/profile/target must be supported by implementations\",\"definition\" : \"If true indicates that the specified type, profile or targetProfile must be supported by implementations.\",\"comment\" : \"An element may be labelled as must support. This extension clarifies which types/profiles/targetProfiles are must-support. It has no meaning if the element itself is not must-support. If the element is labelled must-support, and none of the options are labelled as must support, then an application must support at least one of the possible options, but is not required to support all of them.\",\"min\" : 0,\"max\" : \"1\",\"base\" : {\"path\" : \"Extension\",\"min\" : 0,\"max\" : \"*\"},\"constraint\" : [{\"key\" : \"ele-1\",\"severity\" : \"error\",\"human\" : \"All FHIR elements must have a @value or children\",\"expression\" : \"hasValue() or (children().count() > id.count())\",\"xpath\" : \"@value|f:*|h:div\",\"source\" : \"http://hl7.org/fhir/StructureDefinition/Element\"},{\"key\" : \"ext-1\",\"severity\" : \"error\",\"human\" : \"Must have either extensions or value[x], not both\",\"expression\" : \"extension.exists() != value.exists()\",\"xpath\" : \"exists(f:extension)!=exists(f:*[starts-with(local-name(.), 'value')])\",\"source\" : \"http://hl7.org/fhir/StructureDefinition/Extension\"}],\"isModifier\" : false},{\"id\" : \"Extension.id\",\"path\" : \"Extension.id\",\"representation\" : [\"xmlAttr\"],\"short\" : \"Unique id for inter-element referencing\",\"definition\" : \"Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.\",\"min\" : 0,\"max\" : \"1\",\"base\" : {\"path\" : \"Element.id\",\"min\" : 0,\"max\" : \"1\"},\"type\" : [{\"extension\" : [{\"url\" : \"http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type\",\"valueUri\" : \"string\"}],\"code\" : \"http://hl7.org/fhirpath/System.String\"}],\"isModifier\" : false,\"isSummary\" : false,\"mapping\" : [{\"identity\" : \"rim\",\"map\" : \"n/a\"}]},{\"id\" : \"Extension.extension\",\"path\" : \"Extension.extension\",\"slicing\" : {\"discriminator\" : [{\"type\" : \"value\",\"path\" : \"url\"}],\"description\" : \"Extensions are always sliced by (at least) url\",\"rules\" : \"open\"},\"short\" : \"Extension\",\"definition\" : \"An Extension\",\"min\" : 0,\"max\" : \"0\",\"base\" : {\"path\" : \"Element.extension\",\"min\" : 0,\"max\" : \"*\"},\"type\" : [{\"code\" : \"Extension\"}],\"constraint\" : [{\"key\" : \"ele-1\",\"severity\" : \"error\",\"human\" : \"All FHIR elements must have a @value or children\",\"expression\" : \"hasValue() or (children().count() > id.count())\",\"xpath\" : \"@value|f:*|h:div\",\"source\" : \"http://hl7.org/fhir/StructureDefinition/Element\"},{\"key\" : \"ext-1\",\"severity\" : \"error\",\"human\" : \"Must have either extensions or value[x], not both\",\"expression\" : \"extension.exists() != value.exists()\",\"xpath\" : \"exists(f:extension)!=exists(f:*[starts-with(local-name(.), \\\"value\\\")])\",\"source\" : \"http://hl7.org/fhir/StructureDefinition/Extension\"}],\"isModifier\" : false,\"isSummary\" : false},{\"id\" : \"Extension.url\",\"path\" : \"Extension.url\",\"representation\" : [\"xmlAttr\"],\"short\" : \"identifies the meaning of the extension\",\"definition\" : \"Source of the definition for the extension code - a logical name or a URL.\",\"comment\" : \"The definition may point directly to a computable or human-readable definition of the extensibility codes, or it may be a logical URI as declared in some other specification. The definition SHALL be a URI for the Structure Definition defining the extension.\",\"min\" : 1,\"max\" : \"1\",\"base\" : {\"path\" : \"Extension.url\",\"min\" : 1,\"max\" : \"1\"},\"type\" : [{\"extension\" : [{\"url\" : \"http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type\",\"valueUri\" : \"uri\"}],\"code\" : \"http://hl7.org/fhirpath/System.String\"}],\"fixedUri\" : \"http://hl7.org/fhir/StructureDefinition/elementdefinition-type-must-support\",\"isModifier\" : false,\"isSummary\" : false,\"mapping\" : [{\"identity\" : \"rim\",\"map\" : \"N/A\"}]},{\"id\" : \"Extension.value[x]\",\"path\" : \"Extension.value[x]\",\"short\" : \"Value of extension\",\"definition\" : \"Value of extension - must be one of a constrained set of the data types (see [Extensibility](extensibility.html) for a list).\",\"min\" : 1,\"max\" : \"1\",\"base\" : {\"path\" : \"Extension.value[x]\",\"min\" : 0,\"max\" : \"1\"},\"type\" : [{\"code\" : \"boolean\"}],\"constraint\" : [{\"key\" : \"ele-1\",\"severity\" : \"error\",\"human\" : \"All FHIR elements must have a @value or children\",\"expression\" : \"hasValue() or (children().count() > id.count())\",\"xpath\" : \"@value|f:*|h:div\",\"source\" : \"http://hl7.org/fhir/StructureDefinition/Element\"}],\"isModifier\" : false,\"isSummary\" : false,\"mapping\" : [{\"identity\" : \"rim\",\"map\" : \"N/A\"}]}]},\"differential\" : {\"element\" : [{\"id\" : \"Extension\",\"path\" : \"Extension\",\"short\" : \"The specified type/profile/target must be supported by implementations\",\"definition\" : \"If true indicates that the specified type, profile or targetProfile must be supported by implementations.\",\"comment\" : \"An element may be labelled as must support. This extension clarifies which types/profiles/targetProfiles are must-support. It has no meaning if the element itself is not must-support. If the element is labelled must-support, and none of the options are labelled as must support, then an application must support at least one of the possible options, but is not required to support all of them.\",\"min\" : 0,\"max\" : \"1\"},{\"id\" : \"Extension.extension\",\"path\" : \"Extension.extension\",\"max\" : \"0\"},{\"id\" : \"Extension.url\",\"path\" : \"Extension.url\",\"fixedUri\" : \"http://hl7.org/fhir/StructureDefinition/elementdefinition-type-must-support\"},{\"id\" : \"Extension.value[x]\",\"path\" : \"Extension.value[x]\",\"min\" : 1,\"type\" : [{\"code\" : \"boolean\"}]}]}}"; + + public static boolean isKnownExtension(String url) { + return Utilities.existsInList(url, + "http://hl7.org/fhir/StructureDefinition/elementdefinition-type-must-support" + ); + } + + public static StructureDefinition getDefinition(String url) { + try { + switch (url) { + case "http://hl7.org/fhir/StructureDefinition/elementdefinition-type-must-support" : return makeMustSupport(); + default: return null; + } + } catch (Exception e) { + return null; + } + } + + private static StructureDefinition makeMustSupport() throws FHIRFormatError, IOException { + return (StructureDefinition) new JsonParser().parse(MUST_SUPPORT_SOURCE); + } + +} 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 d7dec41cd..f7631ef8b 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 @@ -28,6 +28,7 @@ import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule; import org.hl7.fhir.r5.model.SearchParameter; import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.r5.utils.FHIRPathEngine; +import org.hl7.fhir.r5.utils.ToolingExtensions; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.VersionUtilities; import org.hl7.fhir.utilities.i18n.I18nConstants; @@ -95,6 +96,69 @@ public class StructureDefinitionValidator extends BaseValidator { } catch (FHIRException | IOException e) { rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.ERROR_GENERATING_SNAPSHOT, e.getMessage()); } + List differentials = src.getChildrenByName("differential"); + List snapshots = src.getChildrenByName("snapshot"); + for (Element differential : differentials) { + validateElementList(errors, differential, stack.push(differential, -1, null, null), false, snapshots.size() > 0); + } + for (Element snapshot : snapshots) { + validateElementList(errors, snapshot, stack.push(snapshot, -1, null, null), true, true); + } + } + + private void validateElementList(List errors, Element elementList, NodeStack stack, boolean snapshot, boolean hasSnapshot) { + List elements = elementList.getChildrenByName("element"); + int cc = 0; + for (Element element : elements) { + validateElementDefinition(errors, element, stack.push(element, cc, null, null), snapshot, hasSnapshot); + cc++; + } + } + + private void validateElementDefinition(List errors, Element element, NodeStack stack, boolean snapshot, boolean hasSnapshot) { + boolean typeMustSupport = false; + List types = element.getChildrenByName("type"); + for (Element type : types) { + if (hasMustSupportExtension(type)) { + typeMustSupport = true; + } + } + if (typeMustSupport) { + if (snapshot) { + rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), "true".equals(element.getChildValue("mustSupport")), I18nConstants.SD_NESTED_MUST_SUPPORT_SNAPSHOT, element.getNamedChildValue("path")); + } else { + hint(errors, IssueType.EXCEPTION, stack.getLiteralPath(), hasSnapshot || "true".equals(element.getChildValue("mustSupport")), I18nConstants.SD_NESTED_MUST_SUPPORT_DIFF, element.getNamedChildValue("path")); + } + } + } + + private boolean hasMustSupportExtension(Element type) { + if ("true".equals(getExtensionValue(type, ToolingExtensions.EXT_MUST_SUPPORT))) { + return true; + } + List profiles = type.getChildrenByName("profile"); + for (Element profile : profiles) { + if ("true".equals(getExtensionValue(profile, ToolingExtensions.EXT_MUST_SUPPORT))) { + return true; + } + } + profiles = type.getChildrenByName("targetProfile"); + for (Element profile : profiles) { + if ("true".equals(getExtensionValue(profile, ToolingExtensions.EXT_MUST_SUPPORT))) { + return true; + } + } + return false; + } + + private String getExtensionValue(Element element, String url) { + List extensions = element.getChildrenByName("extension"); + for (Element extension : extensions) { + if (url.equals(extension.getNamedChildValue("url"))) { + return extension.getNamedChildValue("value"); + } + } + return null; } private StructureDefinition loadAsSD(Element src) throws FHIRException, IOException {