Support for CDA code validation

This commit is contained in:
Grahame Grieve 2023-10-12 12:54:25 +11:00
parent a10ac3c590
commit 1207e32e20
3 changed files with 84 additions and 46 deletions

View File

@ -1006,6 +1006,8 @@ public class I18nConstants {
public static final String FHIRPATH_CHOICE_NO_TYPE_SPECIFIER = "FHIRPATH_CHOICE_NO_TYPE_SPECIFIER"; public static final String FHIRPATH_CHOICE_NO_TYPE_SPECIFIER = "FHIRPATH_CHOICE_NO_TYPE_SPECIFIER";
public static final String FHIRPATH_CHOICE_SPURIOUS_TYPE_SPECIFIER = "FHIRPATH_CHOICE_SPURIOUS_TYPE_SPECIFIER"; public static final String FHIRPATH_CHOICE_SPURIOUS_TYPE_SPECIFIER = "FHIRPATH_CHOICE_SPURIOUS_TYPE_SPECIFIER";
public static final String FHIRPATH_NOT_A_COLLECTION = "FHIRPATH_NOT_A_COLLECTION"; public static final String FHIRPATH_NOT_A_COLLECTION = "FHIRPATH_NOT_A_COLLECTION";
public static final String TERMINOLOGY_TX_UNKNOWN_OID = "TERMINOLOGY_TX_UNKNOWN_OID";
public static final String TERMINOLOGY_TX_NO_SYSTEM = "TERMINOLOGY_TX_NO_SYSTEM";
} }

View File

@ -1063,4 +1063,6 @@ LOGICAL_MODEL_QNAME_MISMATCH = The QName ''{0}'' does not match the expected QNa
FHIRPATH_CHOICE_NO_TYPE_SPECIFIER = The expression ''{0}'' refers to an element that is a choice, but doesn''t have an .ofType() so that SQL view runners can pre-determine the full element name FHIRPATH_CHOICE_NO_TYPE_SPECIFIER = The expression ''{0}'' refers to an element that is a choice, but doesn''t have an .ofType() so that SQL view runners can pre-determine the full element name
FHIRPATH_CHOICE_SPURIOUS_TYPE_SPECIFIER = The expression ''{0}'' refers to an element that is not a choice, but has an .ofType(). SQL view runners are likely to pre-determine an incorrect full element name FHIRPATH_CHOICE_SPURIOUS_TYPE_SPECIFIER = The expression ''{0}'' refers to an element that is not a choice, but has an .ofType(). SQL view runners are likely to pre-determine an incorrect full element name
FHIRPATH_NOT_A_COLLECTION = Found a use of a collection operator on something that is not a collection at ''{0}'' - check that there's no mistakes in the expression syntax FHIRPATH_NOT_A_COLLECTION = Found a use of a collection operator on something that is not a collection at ''{0}'' - check that there's no mistakes in the expression syntax
TERMINOLOGY_TX_UNKNOWN_OID = The OID ''{0}'' is not known
TERMINOLOGY_TX_NO_SYSTEM = In the absence of a code system, the code cannot be validated

View File

@ -93,6 +93,7 @@ import org.hl7.fhir.r5.model.CanonicalResource;
import org.hl7.fhir.r5.model.CanonicalType; import org.hl7.fhir.r5.model.CanonicalType;
import org.hl7.fhir.r5.model.CodeSystem; import org.hl7.fhir.r5.model.CodeSystem;
import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent;
import org.hl7.fhir.r5.model.CodeType;
import org.hl7.fhir.r5.model.CodeableConcept; import org.hl7.fhir.r5.model.CodeableConcept;
import org.hl7.fhir.r5.model.Coding; import org.hl7.fhir.r5.model.Coding;
import org.hl7.fhir.r5.model.ContactPoint; import org.hl7.fhir.r5.model.ContactPoint;
@ -1403,7 +1404,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
} }
private boolean checkTerminologyCodeableConcept(List<ValidationMessage> errors, String path, Element element, StructureDefinition profile, ElementDefinition theElementCntext, NodeStack stack, StructureDefinition logical) { private boolean checkCDACodeableConcept(List<ValidationMessage> errors, String path, Element element, StructureDefinition profile, ElementDefinition theElementCntext, NodeStack stack, StructureDefinition logical) {
boolean ok = true; boolean ok = true;
if (!noTerminologyChecks && theElementCntext != null && theElementCntext.hasBinding()) { if (!noTerminologyChecks && theElementCntext != null && theElementCntext.hasBinding()) {
ElementDefinitionBindingComponent binding = theElementCntext.getBinding(); ElementDefinitionBindingComponent binding = theElementCntext.getBinding();
@ -1419,7 +1420,8 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
} }
} else { } else {
try { try {
CodeableConcept cc = convertToCodeableConcept(element, logical); CodeableConcept cc = new CodeableConcept();
ok = convertCDACodeToCodeableConcept(errors, path, element, logical, cc) && ok;
if (!cc.hasCoding()) { if (!cc.hasCoding()) {
if (binding.getStrength() == BindingStrength.REQUIRED) if (binding.getStrength() == BindingStrength.REQUIRED)
ok = rule(errors, NO_RULE_DATE, IssueType.CODEINVALID, element.line(), element.col(), path, false, "No code provided, and a code is required from the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getVersionedUrl()) && ok; ok = rule(errors, NO_RULE_DATE, IssueType.CODEINVALID, element.line(), element.col(), path, false, "No code provided, and a code is required from the value set " + describeReference(binding.getValueSet()) + " (" + valueset.getVersionedUrl()) && ok;
@ -1638,43 +1640,35 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
return ok; return ok;
} }
private CodeableConcept convertToCodeableConcept(Element element, StructureDefinition logical) { private boolean convertCDACodeToCodeableConcept(List<ValidationMessage> errors, String path, Element element, StructureDefinition logical, CodeableConcept cc) {
CodeableConcept res = new CodeableConcept(); boolean ok = true;
for (ElementDefinition ed : logical.getSnapshot().getElement()) { cc.setText(element.getNamedChildValue("originalText"));
if (Utilities.charCount(ed.getPath(), '.') == 1) { if (element.hasChild("nullFlavor")) {
List<String> maps = getMapping("http://hl7.org/fhir/terminology-pattern", logical, ed); cc.addExtension("http://hl7.org/fhir/StructureDefinition/iso21090-nullFlavor", new CodeType(element.getNamedChildValue("nullFlavor")));
for (String m : maps) { }
String name = tail(ed.getPath()); if (element.hasChild("code") || element.hasChild("codeSystem")) {
List<Element> list = new ArrayList<>(); Coding c = cc.addCoding();
element.getNamedChildren(name, list);
if (!list.isEmpty()) { String oid = element.getNamedChildValue("codeSystem");
if ("Coding.code".equals(m)) { if (oid != null) {
res.getCodingFirstRep().setCode(list.get(0).primitiveValue()); String url = context.urlForOid(true, oid);
} else if ("Coding.system[fmt:OID]".equals(m)) { if (url == null) {
String oid = list.get(0).primitiveValue(); c.setSystem("urn:oid:"+oid);
String url = new ContextUtilities(context).oid2Uri(oid); ok = false;
if (url != null) { rule(errors, "2023-10-11", IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_UNKNOWN_OID, oid);
res.getCodingFirstRep().setSystem(url);
} else { } else {
res.getCodingFirstRep().setSystem("urn:oid:" + oid); c.setSystem(url);
} }
} else if ("Coding.version".equals(m)) { } else {
res.getCodingFirstRep().setVersion(list.get(0).primitiveValue()); warning(errors, "2023-10-11", IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NO_SYSTEM);
} else if ("Coding.display".equals(m)) {
res.getCodingFirstRep().setDisplay(list.get(0).primitiveValue());
} else if ("CodeableConcept.text".equals(m)) {
res.setText(list.get(0).primitiveValue());
} else if ("CodeableConcept.coding".equals(m)) {
StructureDefinition c = context.fetchTypeDefinition(ed.getTypeFirstRep().getCode());
for (Element e : list) {
res.addCoding(convertToCoding(e, c));
} }
c.setCode(element.getNamedChildValue("code"));
c.setVersion(element.getNamedChildValue("codeSystemVersion"));
c.setDisplay(element.getNamedChildValue("displayName"));
} }
} // todo: translations
} return ok;
}
}
return res;
} }
private Coding convertToCoding(Element element, StructureDefinition logical) { private Coding convertToCoding(Element element, StructureDefinition logical) {
@ -1842,6 +1836,9 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
private boolean checkCoding(List<ValidationMessage> errors, String path, Element element, StructureDefinition profile, ElementDefinition theElementCntext, boolean inCodeableConcept, boolean checkDisplay, NodeStack stack) { private boolean checkCoding(List<ValidationMessage> errors, String path, Element element, StructureDefinition profile, ElementDefinition theElementCntext, boolean inCodeableConcept, boolean checkDisplay, NodeStack stack) {
String code = element.getNamedChildValue("code"); String code = element.getNamedChildValue("code");
String system = element.getNamedChildValue("system"); String system = element.getNamedChildValue("system");
if (code != null && system == null) {
warning(errors, "2023-10-11", IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NO_SYSTEM);
}
String version = element.getNamedChildValue("version"); String version = element.getNamedChildValue("version");
String display = element.getNamedChildValue("display"); String display = element.getNamedChildValue("display");
return checkCodedElement(errors, path, element, profile, theElementCntext, inCodeableConcept, checkDisplay, stack, code, system, version, display); return checkCodedElement(errors, path, element, profile, theElementCntext, inCodeableConcept, checkDisplay, stack, code, system, version, display);
@ -5969,7 +5966,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
String thisExtension = null; String thisExtension = null;
boolean checkDisplay = true; boolean checkDisplay = true;
SpecialElement special = ei.getElement().getSpecial(); // SpecialElement special = ei.getElement().getSpecial();
// this used to say // this used to say
// if (special == SpecialElement.BUNDLE_ENTRY || special == SpecialElement.BUNDLE_OUTCOME || special == SpecialElement.BUNDLE_ISSUES || special == SpecialElement.PARAMETER) { // if (special == SpecialElement.BUNDLE_ENTRY || special == SpecialElement.BUNDLE_OUTCOME || special == SpecialElement.BUNDLE_ISSUES || special == SpecialElement.PARAMETER) {
// ok = checkInvariants(valContext, errors, profile, typeDefn != null ? typeDefn : checkDefn, ei.getElement(), ei.getElement(), localStack, false) && ok; // ok = checkInvariants(valContext, errors, profile, typeDefn != null ? typeDefn : checkDefn, ei.getElement(), ei.getElement(), localStack, false) && ok;
@ -6030,13 +6027,17 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
// (str.matches(".*([.,/])work\\1$")) // (str.matches(".*([.,/])work\\1$"))
} else if (Utilities.isAbsoluteUrl(type)) { } else if (Utilities.isAbsoluteUrl(type)) {
StructureDefinition defn = context.fetchTypeDefinition(type); StructureDefinition defn = context.fetchTypeDefinition(type);
if (defn != null && hasMapping("http://hl7.org/fhir/terminology-pattern", defn, defn.getSnapshot().getElementFirstRep())) { if (defn != null && defn.hasExtension(ToolingExtensions.EXT_BINDING_STYLE)) {
List<String> txtype = getMapping("http://hl7.org/fhir/terminology-pattern", defn, defn.getSnapshot().getElementFirstRep()); String style = ToolingExtensions.readStringExtension(defn, ToolingExtensions.EXT_BINDING_STYLE);
if (txtype.contains("CodeableConcept")) { if ("CDA".equals(style)) {
ok = checkTerminologyCodeableConcept(errors, ei.getPath(), ei.getElement(), profile, checkDefn, stack, defn) && ok; if (cdaTypeIs(defn, "CS")) {
ok = checkCDACodeSimple(valContext, errors, ei.getPath(), ei.getElement(), profile, checkDefn, stack, defn) && ok;
} else if (cdaTypeIs(defn, "CV") || cdaTypeIs(defn, "PQ")) {
ok = checkCDACoding(errors, ei.getPath(), cdaTypeIs(defn, "PQ"), ei.getElement(), profile, checkDefn, stack, defn, inCodeableConcept, checkDisplayInContext) && ok;
} else if (cdaTypeIs(defn, "CD") || cdaTypeIs(defn, "CE")) {
ok = checkCDACodeableConcept(errors, ei.getPath(), ei.getElement(), profile, checkDefn, stack, defn) && ok;
thisIsCodeableConcept = true; thisIsCodeableConcept = true;
} else if (txtype.contains("Coding")) { }
ok = checkTerminologyCoding(errors, ei.getPath(), ei.getElement(), profile, checkDefn, inCodeableConcept, checkDisplayInContext, stack, defn) && ok;
} }
} }
} }
@ -6141,6 +6142,39 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
return ok; return ok;
} }
private boolean cdaTypeIs(StructureDefinition defn, String type) {
return ("http://hl7.org/cda/stds/core/StructureDefinition/"+type).equals(defn.getUrl());
}
private boolean checkCDACoding(List<ValidationMessage> errors, String path, boolean isPQ, Element element, StructureDefinition profile, ElementDefinition checkDefn, NodeStack stack, StructureDefinition defn, boolean inCodeableConcept, boolean checkDisplay) {
boolean ok = true;
String system = null;
String oid = element.getNamedChildValue("codeSystem");
if (oid != null) {
String url = context.urlForOid(true, oid);
if (url == null) {
system = "urn:oid:"+oid;
ok = false;
rule(errors, "2023-10-11", IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_UNKNOWN_OID, oid);
} else {
system = url;
}
}
String code = element.getNamedChildValue(isPQ ? "unit" : "code");
String version = element.getNamedChildValue("codeSystemVersion");
String display = element.getNamedChildValue("displayName");
return checkCodedElement(errors, path, element, profile, checkDefn, inCodeableConcept, checkDisplay, stack, code, system, version, display) && ok;
}
private boolean checkCDACodeSimple(ValidationContext valContext, List<ValidationMessage> errors, String path, Element element, StructureDefinition profile, ElementDefinition checkDefn, NodeStack stack, StructureDefinition defn) {
if (element.hasChild("code")) {
return checkPrimitiveBinding(valContext, errors, path, "code", checkDefn, element.getNamedChild("code"), profile, stack);
} else {
return false;
}
}
private boolean isAbstractType(String type) { private boolean isAbstractType(String type) {
StructureDefinition sd = context.fetchTypeDefinition(type); StructureDefinition sd = context.fetchTypeDefinition(type);
return sd != null && sd.getAbstract(); return sd != null && sd.getAbstract();