From 33a1814d4e605ce79eb0d68085a071b3c837bd76 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Fri, 8 Mar 2024 22:32:55 +1100 Subject: [PATCH] CodeSystem validation - properties and designations --- .../fhir/utilities/i18n/I18nConstants.java | 15 + .../src/main/resources/Messages.properties | 19 +- .../instance/type/CodeSystemValidator.java | 270 +++++++++++++++++- 3 files changed, 293 insertions(+), 11 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 d01897163..a21e66a52 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 @@ -1072,6 +1072,21 @@ public class I18nConstants { public static final String CODESYSTEM_SUPP_NO_DISPLAY = "CODESYSTEM_SUPP_NO_DISPLAY"; public static final String CODESYSTEM_NOT_CONTAINED = "CODESYSTEM_NOT_CONTAINED"; public static final String CODESYSTEM_THO_CHECK = "CODESYSTEM_THO_CHECK"; + public static final String TYPE_SPECIFIC_CHECKS_DT_CANONICAL_MULTIPLE_POSSIBLE_VERSIONS = "TYPE_SPECIFIC_CHECKS_DT_CANONICAL_MULTIPLE_POSSIBLE_VERSIONS"; + public static final String CODESYSTEM_PROPERTY_DUPLICATE_URI = "CODESYSTEM_PROPERTY_DUPLICATE_URI"; + public static final String CODESYSTEM_PROPERTY_BAD_HL7_URI = "CODESYSTEM_PROPERTY_BAD_HL7_URI"; + public static final String CODESYSTEM_PROPERTY_SYNONYM_DEPRECATED = "CODESYSTEM_PROPERTY_SYNONYM_DEPRECATED"; + public static final String CODESYSTEM_PROPERTY_DUPLICATE_CODE = "CODESYSTEM_PROPERTY_DUPLICATE_CODE"; + public static final String CODESYSTEM_PROPERTY_URI_CODE_MISMATCH = "CODESYSTEM_PROPERTY_URI_CODE_MISMATCH"; + public static final String CODESYSTEM_PROPERTY_URI_TYPE_MISMATCH = "CODESYSTEM_PROPERTY_URI_TYPE_MISMATCH"; + public static final String CODESYSTEM_PROPERTY_UNKNOWN_CODE = "CODESYSTEM_PROPERTY_UNKNOWN_CODE"; + public static final String CODESYSTEM_PROPERTY_KNOWN_CODE_SUGGESTIVE = "CODESYSTEM_PROPERTY_KNOWN_CODE_SUGGESTIVE"; + public static final String CODESYSTEM_PROPERTY_CODE_TYPE_MISMATCH = "CODESYSTEM_PROPERTY_CODE_TYPE_MISMATCH"; + public static final String CODESYSTEM_PROPERTY_UNDEFINED = "CODESYSTEM_PROPERTY_UNDEFINED"; + public static final String CODESYSTEM_PROPERTY_NO_VALUE = "CODESYSTEM_PROPERTY_NO_VALUE"; + public static final String CODESYSTEM_PROPERTY_WRONG_TYPE = "CODESYSTEM_PROPERTY_WRONG_TYPE"; + public static final String CODESYSTEM_DESIGNATION_DISP_CLASH_NO_LANG = "CODESYSTEM_DESIGNATION_DISP_CLASH_NO_LANG"; + public static final String CODESYSTEM_DESIGNATION_DISP_CLASH_LANG = "CODESYSTEM_DESIGNATION_DISP_CLASH_LANG"; } diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index 125dcd0ea..7a17d7886 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -1109,7 +1109,7 @@ Validation_VAL_Profile_Minimum_SLICE_other = Slice ''{3}'': minimum required = { FHIRPATH_UNKNOWN_EXTENSION = Reference to an unknown extension - double check that the URL ''{0}'' is correct Type_Specific_Checks_DT_XHTML_Resolve = Hyperlink ''{0}'' at ''{1}'' for ''{2}''' does not resolve Type_Specific_Checks_DT_XHTML_Resolve_Img = Image source ''{0}'' at ''{1}'' does not resolve -TYPE_SPECIFIC_CHECKS_DT_XHTML_MULTIPLE_MATCHES = Hyperlink ''{0}'' at ''{1}'' for ''{2}''' resolves to multiple targets +TYPE_SPECIFIC_CHECKS_DT_XHTML_MULTIPLE_MATCHES = Hyperlink ''{0}'' at ''{1}'' for ''{2}'' resolves to multiple targets ({3}) CONTAINED_ORPHAN_DOM3 = The contained resource ''{0}'' is not referenced to from elsewhere in the containing resource nor does it refer to the containing resource (dom-3) VALUESET_INCLUDE_CS_NOT_CS = The include system ''{0}'' is a reference to a contained resource, but the contained resource with that id is not a CodeSystem, it's a {1} VALUESET_INCLUDE_CS_NOT_FOUND = No matching contained code system found for system ''{0}'' @@ -1129,4 +1129,19 @@ VALUESET_INCLUDE_CSVER_SUPPLEMENT = The value set references CodeSystem ''{0}'' CODESYSTEM_SUPP_NO_DISPLAY = This display (''{0}'') differs from that defined by the base code system (''{1}''). Both displays claim to be 'the "primary designation" for the same language (''{2}''), and the correct interpretation of this is undefined CODESYSTEM_NOT_CONTAINED = CodeSystems are referred to directly from Coding.system, so it's generally best for them not to be contained resources CODESYSTEM_THO_CHECK = Most code systems defined in HL7 IGs will need to move to THO later during the process. Consider giving this code system a THO URL now (See https://confluence.hl7.org/display/TSMG/Terminology+Play+Book) - \ No newline at end of file +TYPE_SPECIFIC_CHECKS_DT_CANONICAL_MULTIPLE_POSSIBLE_VERSIONS = There are multiple different potential matches for the url ''{0}''. It might be a good idea to fix to the correct version to reduce the likelihood of a wrong version being selected by an implementation/implementer. Using version ''{1}'', found versions: {2} +ABSTRACT_CODE_NOT_ALLOWED = Code ''{0}#{1}'' is abstract, and not allowed in this context +CODESYSTEM_PROPERTY_DUPLICATE_URI = A property is already defined with the URI ''{0}'' +CODESYSTEM_PROPERTY_BAD_HL7_URI = Unknown CodeSystem Property ''{0}''. If you are creating your own property, do not create it in the HL7 namespace +CODESYSTEM_PROPERTY_SYNONYM_DEPRECATED = The 'synonym' property is deprecated; just create duplicate concepts +CODESYSTEM_PROPERTY_DUPLICATE_CODE = A property is already defined with the code ''{0}'' +CODESYSTEM_PROPERTY_URI_CODE_MISMATCH = The URI ''{0}'' is normally assigned the code ''{1}''. Using the code ''{2}'' will usually create confusion in ValueSet filters etc +CODESYSTEM_PROPERTY_URI_TYPE_MISMATCH = Wrong type ''{2}'': The URI ''{0}'' identifies a property that has the type ''{1}'' +CODESYSTEM_PROPERTY_UNKNOWN_CODE = This property has only (''{0}'') a code and no URI, so it has no clearly defined meaning in the terminology ecosystem +CODESYSTEM_PROPERTY_KNOWN_CODE_SUGGESTIVE = This property has only the standard code (''{0}'') but not the standard URI ''{1}'', so it has no clearly defined meaning in the terminology ecosystem +CODESYSTEM_PROPERTY_CODE_TYPE_MISMATCH = Wrong type ''{2}'': The code ''{0}'' identifies a property that has the type ''{1}'' +CODESYSTEM_PROPERTY_UNDEFINED = The property ''{0}'' has no definition. Many terminology tools won't know what to do with it +CODESYSTEM_PROPERTY_NO_VALUE = The property ''{0}'' has no value, and cannot be understoof +CODESYSTEM_PROPERTY_WRONG_TYPE = The property ''{0}'' has the invalid type ''{1}'', when it is defined to have the type ''{2}'' +CODESYSTEM_DESIGNATION_DISP_CLASH_NO_LANG = The designation ''{0}'' has no use and no language, so is not differentiated from the base display (''{1}'') +CODESYSTEM_DESIGNATION_DISP_CLASH_LANG = The designation ''{0}'' has no use and is in the same language (''{2}''), so is not differentiated from the base display (''{1}'') diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/CodeSystemValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/CodeSystemValidator.java index 0314e51a2..6e08cd623 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/CodeSystemValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/CodeSystemValidator.java @@ -1,7 +1,10 @@ package org.hl7.fhir.validation.instance.type; +import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import org.hl7.fhir.exceptions.FHIRException; @@ -16,11 +19,66 @@ import org.hl7.fhir.utilities.validation.ValidationMessage; import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; import org.hl7.fhir.utilities.validation.ValidationOptions; import org.hl7.fhir.validation.BaseValidator; +import org.hl7.fhir.validation.instance.type.CodeSystemValidator.KnownProperty; +import org.hl7.fhir.validation.instance.type.CodeSystemValidator.PropertyDef; import org.hl7.fhir.validation.instance.utils.NodeStack; import org.hl7.fhir.validation.instance.utils.ValidationContext; public class CodeSystemValidator extends BaseValidator { + public enum KnownProperty { + Status, Inactive, EffectiveDate, DeprecationDate, RetirementDate, NotSelectable, Parent, Child, PartOf, Synonym, Comment, ItemWeight; + + String getType() { + switch (this) { + case Child: return "code"; + case Comment: return "string"; + case DeprecationDate: return "dateTime"; + case EffectiveDate: return "dateTime"; + case Inactive: return "boolean"; + case ItemWeight: return "decimal"; + case NotSelectable: return "boolean"; + case Parent: return "code"; + case PartOf: return "code"; + case RetirementDate: return "dateTime"; + case Status: return "code"; + case Synonym: return "code"; + default: return null; + } + } + + String getCode() { + return Utilities.uncapitalize(this.toString()); + } + + String getUri() { + return "http://hl7.org/fhir/concept-properties#"+ getCode(); + } + + } + + public class PropertyDef { + private String uri; + private String code; + private String type; + protected PropertyDef(String uri, String code, String type) { + super(); + this.uri = uri; + this.code = code; + this.type = type; + } + public String getUri() { + return uri; + } + public String getCode() { + return code; + } + public String getType() { + return type; + } + + } + public CodeSystemValidator(BaseValidator parent) { super(parent); } @@ -114,12 +172,20 @@ public class CodeSystemValidator extends BaseValidator { hint(errors, "2024-03-06", IssueType.BUSINESSRULE, cs.line(), cs.col(), stack.getLiteralPath(), !isInQ && !isSuppProp, I18nConstants.CODESYSTEM_NOT_CONTAINED); } + Map properties = new HashMap<>(); + List propertyElements = cs.getChildrenByName("property"); + int i = 0; + for (Element propertyElement : propertyElements) { + ok = checkPropertyDefinition(errors, cs, stack.push(propertyElement, i, null, null), "true".equals(caseSensitive), hierarchyMeaning, csB, propertyElement, properties) && ok; + i++; + } + Set codes = new HashSet<>(); List concepts = cs.getChildrenByName("concept"); - int i = 0; + i = 0; for (Element concept : concepts) { - checkConcept(errors, cs, stack.push(concept, i, null, null), "true".equals(caseSensitive), hierarchyMeaning, csB, concept, codes); + ok = checkConcept(errors, cs, stack.push(concept, i, null, null), "true".equals(caseSensitive), hierarchyMeaning, csB, concept, codes, properties) && ok; i++; } @@ -127,7 +193,134 @@ public class CodeSystemValidator extends BaseValidator { } - private void checkConcept(List errors, Element cs, NodeStack stack, boolean caseSensitive, String hierarchyMeaning, CodeSystem csB, Element concept, Set codes) { + private boolean checkPropertyDefinition(List errors, Element cs, NodeStack stack, boolean equals, String hierarchyMeaning, CodeSystem csB, Element property, Map properties) { + boolean ok = true; + String uri = property.getNamedChildValue("uri"); + String code = property.getNamedChildValue("code"); + String type = property.getNamedChildValue("type"); + PropertyDef pd = new PropertyDef(uri, code, type); + KnownProperty ukp = null; + KnownProperty ckp = null; + + if (uri != null) { + if (rule(errors, "2024-03-06", IssueType.BUSINESSRULE, cs.line(), cs.col(), stack.getLiteralPath(), !properties.containsKey(uri), I18nConstants.CODESYSTEM_PROPERTY_DUPLICATE_URI, uri)) { + properties.put(uri, pd); + } else { + ok = false; + } + if (uri.contains("hl7.org/fhir")) { + switch (uri) { + case "http://hl7.org/fhir/concept-properties#status" : + ukp = KnownProperty.Status; + break; + case "http://hl7.org/fhir/concept-properties#inactive" : + ukp = KnownProperty.Inactive; + break; + case "http://hl7.org/fhir/concept-properties#effectiveDate" : + ukp = KnownProperty.EffectiveDate; + break; + case "http://hl7.org/fhir/concept-properties#deprecationDate" : + ukp = KnownProperty.DeprecationDate; + break; + case "http://hl7.org/fhir/concept-properties#retirementDate" : + ukp = KnownProperty.RetirementDate; + break; + case "http://hl7.org/fhir/concept-properties#notSelectable" : + ukp = KnownProperty.NotSelectable; + break; + case "http://hl7.org/fhir/concept-properties#parent" : + ukp = KnownProperty.Parent; + break; + case "http://hl7.org/fhir/concept-properties#child" : + ukp = KnownProperty.Child; + break; + case "http://hl7.org/fhir/concept-properties#partOf" : + ukp = KnownProperty.PartOf; + break; + case "http://hl7.org/fhir/concept-properties#synonym" : + ukp = KnownProperty.Synonym; + break; + case "http://hl7.org/fhir/concept-properties#comment" : + ukp = KnownProperty.Comment; + break; + case "http://hl7.org/fhir/concept-properties#itemWeight" : + ukp = KnownProperty.ItemWeight; + break; + default: + ok = false; + rule(errors, "2024-03-06", IssueType.BUSINESSRULE, cs.line(), cs.col(), stack.getLiteralPath(), false, I18nConstants.CODESYSTEM_PROPERTY_BAD_HL7_URI, uri); + } + warning(errors, "2024-03-06", IssueType.BUSINESSRULE, cs.line(), cs.col(), stack.getLiteralPath(), ukp != KnownProperty.Synonym, I18nConstants.CODESYSTEM_PROPERTY_SYNONYM_DEPRECATED); + } + } + if (code != null) { + if (rule(errors, "2024-03-06", IssueType.BUSINESSRULE, cs.line(), cs.col(), stack.getLiteralPath(), !properties.containsKey(code), I18nConstants.CODESYSTEM_PROPERTY_DUPLICATE_CODE, code)) { + properties.put(code, pd); + } else { + ok = false; + } + switch (code) { + case "status" : + ckp = KnownProperty.Status; + break; + case "inactive" : + ckp = KnownProperty.Inactive; + break; + case "effectiveDate" : + ckp = KnownProperty.EffectiveDate; + break; + case "deprecationDate" : + ckp = KnownProperty.DeprecationDate; + break; + case "retirementDate" : + ckp = KnownProperty.RetirementDate; + break; + case "notSelectable" : + ckp = KnownProperty.NotSelectable; + break; + case "parent" : + ckp = KnownProperty.Parent; + break; + case "child" : + ckp = KnownProperty.Child; + break; + case "partOf" : + ckp = KnownProperty.PartOf; + break; + case "synonym" : + ckp = KnownProperty.Synonym; + break; + case "comment" : + ckp = KnownProperty.Comment; + break; + case "itemWeight" : + ckp = KnownProperty.ItemWeight; + break; + default: + // no rules around codes... + } + } + if (ukp != null) { + ok = rule(errors, "2024-03-06", IssueType.BUSINESSRULE, cs.line(), cs.col(), stack.getLiteralPath(), ckp == null || ckp == ukp, I18nConstants.CODESYSTEM_PROPERTY_URI_CODE_MISMATCH, uri, ukp.getCode(), code) && ok; + if (type != null) { + ok = rule(errors, "2024-03-06", IssueType.BUSINESSRULE, cs.line(), cs.col(), stack.getLiteralPath(), type.equals(ukp.getType()), I18nConstants.CODESYSTEM_PROPERTY_URI_TYPE_MISMATCH, uri, ukp.getType(),type) && ok; + } + } + if (uri == null) { + if (ckp == null) { + hint(errors, "2024-03-06", IssueType.BUSINESSRULE, cs.line(), cs.col(), stack.getLiteralPath(), false, I18nConstants.CODESYSTEM_PROPERTY_UNKNOWN_CODE, code); + } else { + warning(errors, "2024-03-06", IssueType.BUSINESSRULE, cs.line(), cs.col(), stack.getLiteralPath(), false, I18nConstants.CODESYSTEM_PROPERTY_KNOWN_CODE_SUGGESTIVE, code, ckp.getUri()); + if (type != null) { + warning(errors, "2024-03-06", IssueType.BUSINESSRULE, cs.line(), cs.col(), stack.getLiteralPath(), type.equals(ckp.getType()), I18nConstants.CODESYSTEM_PROPERTY_CODE_TYPE_MISMATCH, code, ckp.getType(), type); + } + } + } + return ok; + } + + private boolean checkConcept(List errors, Element cs, NodeStack stack, boolean caseSensitive, String hierarchyMeaning, CodeSystem csB, Element concept, Set codes, Map properties) { + boolean ok = true; String code = concept.getNamedChildValue("code"); String display = concept.getNamedChildValue("display"); @@ -145,16 +338,75 @@ public class CodeSystemValidator extends BaseValidator { // todo: check that all the properties are defined. // check that all the defined properties have values - // check the designations have values, and the use/language don't conflict - - - List concepts = concept.getChildrenByName("concept"); + // check the designations have values, and the use/language don't conflict + + List propertyElements = concept.getChildrenByName("property"); int i = 0; - for (Element child : concepts) { - checkConcept(errors, cs, stack.push(concept, i, null, null), caseSensitive, hierarchyMeaning, csB, child, codes); + for (Element propertyElement : propertyElements) { + ok = checkPropertyValue(errors, cs, stack.push(propertyElement, i, null, null), propertyElement, properties) && ok; + i++; + } + + List designations = concept.getChildrenByName("designation"); + i = 0; + for (Element designation : designations) { + ok = checkDesignation(errors, cs, stack.push(designation, i, null, null), concept, designation) && ok; i++; } + List concepts = concept.getChildrenByName("concept"); + i = 0; + for (Element child : concepts) { + ok = checkConcept(errors, cs, stack.push(concept, i, null, null), caseSensitive, hierarchyMeaning, csB, child, codes, properties) && ok; + i++; + } + return ok; + } + + private boolean checkDesignation(List errors, Element cs, NodeStack stack, Element concept, Element designation) { + boolean ok = true; + + String rlang = cs.getNamedChildValue("language"); + String display = concept.getNamedChildValue("display"); + String lang = designation.getNamedChildValue("language"); + List uses = new ArrayList(); + designation.getNamedChildren("additionalUse", uses); + Element use = designation.getNamedChild("use"); + if (use != null) { + uses.add(0, use); + } + String value = designation.getNamedChildValue("value"); + + if (uses.isEmpty()) { + // if we have no uses, we're kind of implying that it's the base display, so it should be the same + if (rlang == null && lang == null) { + ok = rule(errors, "2024-03-06", IssueType.BUSINESSRULE, cs.line(), cs.col(), stack.getLiteralPath(), display == null || display.equals(value), I18nConstants.CODESYSTEM_DESIGNATION_DISP_CLASH_NO_LANG, value, display) && ok; + } else if (rlang != null && ((lang == null) || rlang.equals(lang))) { + ok = rule(errors, "2024-03-06", IssueType.BUSINESSRULE, cs.line(), cs.col(), stack.getLiteralPath(), display == null || display.equals(value), I18nConstants.CODESYSTEM_DESIGNATION_DISP_CLASH_LANG, value, display, rlang) && ok; + } + } else { + // .... do we care? + } + + return ok; + } + + private boolean checkPropertyValue(List errors, Element cs, NodeStack stack, Element property, Map properties) { + boolean ok = true; + + String code = property.getNamedChildValue("code"); + Element value = property.getNamedChild("value"); + if (code != null) { + PropertyDef defn = properties.get(code); + if (rule(errors, "2024-03-06", IssueType.BUSINESSRULE, cs.line(), cs.col(), stack.getLiteralPath(), defn != null, I18nConstants.CODESYSTEM_PROPERTY_UNDEFINED, code) && + rule(errors, "2024-03-06", IssueType.BUSINESSRULE, cs.line(), cs.col(), stack.getLiteralPath(), value != null, I18nConstants.CODESYSTEM_PROPERTY_NO_VALUE, code) && + rule(errors, "2024-03-06", IssueType.BUSINESSRULE, cs.line(), cs.col(), stack.getLiteralPath(), value.fhirType().equals(defn.type), I18nConstants.CODESYSTEM_PROPERTY_WRONG_TYPE, code, value.fhirType(), defn.type)) { + // nothing? + } else { + ok = false; + } + } + return ok; } private boolean checkShareableCodeSystem(List errors, Element cs, NodeStack stack) {