CodeSystem validation - properties and designations

This commit is contained in:
Grahame Grieve 2024-03-08 22:32:55 +11:00
parent a51c3f96ad
commit 33a1814d4e
3 changed files with 293 additions and 11 deletions

View File

@ -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";
}

View File

@ -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)
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}'')

View File

@ -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<String, PropertyDef> properties = new HashMap<>();
List<Element> 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<String> codes = new HashSet<>();
List<Element> 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<ValidationMessage> errors, Element cs, NodeStack stack, boolean caseSensitive, String hierarchyMeaning, CodeSystem csB, Element concept, Set<String> codes) {
private boolean checkPropertyDefinition(List<ValidationMessage> errors, Element cs, NodeStack stack, boolean equals, String hierarchyMeaning, CodeSystem csB, Element property, Map<String, PropertyDef> 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<ValidationMessage> errors, Element cs, NodeStack stack, boolean caseSensitive, String hierarchyMeaning, CodeSystem csB, Element concept, Set<String> codes, Map<String, PropertyDef> 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<Element> concepts = concept.getChildrenByName("concept");
// check the designations have values, and the use/language don't conflict
List<Element> 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<Element> 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<Element> 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<ValidationMessage> 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<Element> uses = new ArrayList<Element>();
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<ValidationMessage> errors, Element cs, NodeStack stack, Element property, Map<String, PropertyDef> 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<ValidationMessage> errors, Element cs, NodeStack stack) {