fix up LOINC validation for value set filters

This commit is contained in:
Grahame Grieve 2024-05-12 22:28:15 +10:00
parent 3f236eb880
commit 5ae44c23d5
3 changed files with 191 additions and 18 deletions

View File

@ -1093,6 +1093,7 @@ public class I18nConstants {
public static final String VALUESET_BAD_FILTER_VALUE_DECIMAL = "VALUESET_BAD_FILTER_VALUE_DECIMAL";
public static final String VALUESET_BAD_FILTER_VALUE_INTEGER = "VALUESET_BAD_FILTER_VALUE_INTEGER";
public static final String VALUESET_BAD_FILTER_VALUE_VALID_CODE = "VALUESET_BAD_FILTER_VALUE_VALID_CODE";
public static final String VALUESET_BAD_FILTER_VALUE_VALID_CODE_LOINC = "VALUESET_BAD_FILTER_VALUE_VALID_CODE_LOINC";
public static final String VALUESET_BAD_FILTER_VALUE_CODED = "VALUESET_BAD_FILTER_VALUE_CODED";
public static final String VALUESET_BAD_FILTER_VALUE_CODED_INVALID = "VALUESET_BAD_FILTER_VALUE_CODED_INVALID";
public static final String VALUESET_BAD_FILTER_OP = "VALUESET_BAD_FILTER_OP";

View File

@ -1132,6 +1132,7 @@ VALUESET_BAD_FILTER_VALUE_DATETIME = The value for a filter based on property ''
VALUESET_BAD_FILTER_VALUE_DECIMAL = The value for a filter based on property ''{0}'' must be a decimal value, not ''{1}''
VALUESET_BAD_FILTER_VALUE_INTEGER = The value for a filter based on property ''{0}'' must be integer value, not ''{1}''
VALUESET_BAD_FILTER_VALUE_VALID_CODE = The value for a filter based on property ''{0}'' must be a valid code from the system ''{2}'', and ''{1}'' is not ({3})
VALUESET_BAD_FILTER_VALUE_VALID_CODE_LOINC = The value for a filter based on property ''{0}'' looks like a LOINC code, but ''{3}'' is not valid code in LOINC version ''{1}''
VALUESET_BAD_FILTER_VALUE_CODED = The value for a filter based on property ''{0}'' must be in the format system(|version)#code, not ''{1}''
VALUESET_BAD_FILTER_VALUE_CODED_INVALID = The value for a filter based on property ''{0}'' is ''{1}'' which is not a valid code ({2})
VALUESET_BAD_FILTER_OP = The operation ''{0}'' is not allowed for property ''{1}''. Allowed ops: {2}

View File

@ -70,6 +70,7 @@ public class ValueSetValidator extends BaseValidator {
private PropertyFilterType type;
private CodeValidationRule codeValidation;
private EnumSet<PropertyOperation> ops;
private List<String> codeList = new ArrayList<>();
protected PropertyValidationRules(PropertyFilterType type, CodeValidationRule codeValidation, PropertyOperation... ops) {
super();
@ -96,11 +97,20 @@ public class ValueSetValidator extends BaseValidator {
public CodeValidationRule getCodeValidation() {
return codeValidation;
}
public List<String> getCodeList() {
return codeList;
}
public PropertyValidationRules setCodes(String... values) {
for (String v : values) {
codeList.add(v);
}
return this;
}
}
public enum PropertyFilterType {
Boolean, Integer, Decimal, Code, DateTime, Coding
Boolean, Integer, Decimal, Code, DateTime, Coding, CodeList, String, LoincRelationship
}
private static final int TOO_MANY_CODES_TO_VALIDATE = 1000;
@ -433,7 +443,7 @@ public class ValueSetValidator extends BaseValidator {
}
if ("exists".equals(op)) {
ok = checkFilterValue(errors, stack, system, version, ok, property, op, value, PropertyFilterType.Boolean, null) && ok;
ok = checkFilterValue(errors, stack, system, version, ok, property, op, value, new PropertyValidationRules(PropertyFilterType.Boolean, null)) && ok;
} else if ("regex".equals(op)) {
String err = null;
try {
@ -445,10 +455,10 @@ public class ValueSetValidator extends BaseValidator {
ok = rule(errors, "2024-03-09", IssueType.INVALID, stack, !"concept".equals(property), I18nConstants.VALUESET_BAD_PROPERTY_NO_REGEX, property) && ok;
} else if (Utilities.existsInList(op, "in", "not-in")) {
for (String v : value.split("\\,")) {
ok = checkFilterValue(errors, stack, system, version, ok, property, op, v, rules.getType(), rules.getCodeValidation()) && ok;
ok = checkFilterValue(errors, stack, system, version, ok, property, op, v, rules) && ok;
}
} else {
ok = checkFilterValue(errors, stack, system, version, ok, property, op, value, rules.getType(), rules.getCodeValidation()) && ok;
ok = checkFilterValue(errors, stack, system, version, ok, property, op, value, rules) && ok;
}
}
}
@ -474,30 +484,36 @@ public class ValueSetValidator extends BaseValidator {
return false;
}
private boolean checkFilterValue(List<ValidationMessage> errors, NodeStack stack, String system, String version,boolean ok, String property, String op, String value, PropertyFilterType type, CodeValidationRule cr) {
if (type != null) {
private boolean checkFilterValue(List<ValidationMessage> errors, NodeStack stack, String system, String version,boolean ok, String property, String op, String value, PropertyValidationRules rules) {
if (rules.getType() != null) {
if (!Utilities.existsInList(op, "in", "not-in")) {
hint(errors, "2024-03-09", IssueType.INVALID, stack.getLiteralPath(), !value.contains(","), I18nConstants.VALUESET_BAD_FILTER_VALUE_HAS_COMMA, type.toString());
hint(errors, "2024-03-09", IssueType.INVALID, stack.getLiteralPath(), !value.contains(","), I18nConstants.VALUESET_BAD_FILTER_VALUE_HAS_COMMA, rules.getType().toString());
}
switch (type) {
switch (rules.getType()) {
case Boolean:
ok = rule(errors, "2024-03-09", IssueType.INVALID, stack,
Utilities.existsInList(value, "true", "false"),
I18nConstants.VALUESET_BAD_FILTER_VALUE_BOOLEAN, property, value) && ok;
break;
case String:
// nothing to check
break;
case Code:
ok = rule(errors, "2024-03-09", IssueType.INVALID, stack,
value.trim().equals(value),
I18nConstants.VALUESET_BAD_FILTER_VALUE_CODE, property, value) && ok;
if (cr == CodeValidationRule.Error || cr == CodeValidationRule.Warning) {
if (rules.getCodeValidation() == CodeValidationRule.Error || rules.getCodeValidation() == CodeValidationRule.Warning) {
ValidationResult vr = context.validateCode(baseOptions, system, version, value, null);
if (cr == CodeValidationRule.Error) {
if (rules.getCodeValidation() == CodeValidationRule.Error) {
ok = rule(errors, "2024-03-09", IssueType.INVALID, stack.getLiteralPath(), vr.isOk(), I18nConstants.VALUESET_BAD_FILTER_VALUE_VALID_CODE, property, value, system, vr.getMessage()) && ok;
} else {
warning(errors, "2024-03-09", IssueType.INVALID, stack.getLiteralPath(), vr.isOk(), I18nConstants.VALUESET_BAD_FILTER_VALUE_VALID_CODE, property, value, system, vr.getMessage());
}
}
break;
case CodeList:
ok = rule(errors, "2024-05-12", IssueType.INVALID, stack.getLiteralPath(), rules.getCodeList().contains(value), I18nConstants.VALUESET_BAD_FILTER_VALUE_DATETIME, property, value) && ok;
break;
case DateTime:
ok = rule(errors, "2024-03-09", IssueType.INVALID, stack.getLiteralPath(),
value.matches("([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))?)?)?)?"),
@ -522,6 +538,15 @@ public class ValueSetValidator extends BaseValidator {
ok = rule(errors, "2024-03-09", IssueType.INVALID, stack, vr.isOk(), I18nConstants.VALUESET_BAD_FILTER_VALUE_CODED_INVALID, property, value, vr.getMessage()) && ok;
}
break;
case LoincRelationship:
// see https://chat.fhir.org/#narrow/stream/179202-terminology/topic/LOINC.20properties.20in.20filters
// for now, the value can be a LOINC code, or a string value
if (value.matches("(L[A|L|P])?(\\d)+\\-\\d")) {
ValidationResult vr = context.validateCode(baseOptions, system, version, value, null);
ok = rule(errors, "2024-03-09", IssueType.INVALID, stack.getLiteralPath(), vr.isOk(), I18nConstants.VALUESET_BAD_FILTER_VALUE_VALID_CODE_LOINC, property, value, system, vr.getMessage()) && ok;
} else {
// nothing?
}
default:
break;
}
@ -581,13 +606,7 @@ public class ValueSetValidator extends BaseValidator {
}
switch (system) {
case "http://loinc.org" :
if (Utilities.existsInList(property, "copyright", "STATUS", "CLASS", "CONSUMER_NAME", "ORDER_OBS", "DOCUMENT_SECTION", "SCALE_TYP")) {
return new PropertyValidationRules(PropertyFilterType.Code, CodeValidationRule.None);
} else if ("CLASSTYPE".equals(property)) {
return new PropertyValidationRules(PropertyFilterType.Integer, null, addToOps(ops, PropertyOperation.Equals, PropertyOperation.In));
} else {
return new PropertyValidationRules(PropertyFilterType.Code, CodeValidationRule.Error, addToOps(ops, PropertyOperation.Equals, PropertyOperation.In));
}
return getLOINCPropertyDetails(property, ops);
case "http://snomed.info/sct":
switch (property) {
case "constraint": return null; // for now
@ -615,6 +634,93 @@ public class ValueSetValidator extends BaseValidator {
}
private PropertyValidationRules getLOINCPropertyDetails(String property, EnumSet<PropertyOperation> ops) {
if (Utilities.existsInList(property,
"parent",
"child",
"answers-for",
"TIME MODIFIER",
"TIME_ASPCT",
"SYSTEM",
"SUPER SYSTEM",
"SUFFIX",
"SCALE",
"SCALE_TYP",
"Rad.View.View Type",
"Rad.View.Aggregation",
"Rad.Timing",
"Rad.Subject",
"Rad.Reason for Exam",
"Rad.Pharmaceutical.Substance Given",
"Rad.Pharmaceutical.Route",
"Rad.Modality.Modality Type",
"Rad.Modality.Modality Subtype",
"Rad.Maneuver.Maneuver Type",
"Rad.Guidance for.Presence",
"Rad.Guidance for.Object",
"Rad.Guidance for.Approach",
"Rad.Guidance for.Action",
"Rad.Anatomic Location.Region Imaged",
"Rad.Anatomic Location.Laterality.Presence",
"Rad.Anatomic Location.Laterality",
"Rad.Anatomic Location.Imaging Focus",
"PROPERTY",
"METHOD",
"GENE",
"Document.TypeOfService",
"Document.SubjectMatterDomain",
"Document.Setting",
"Document.Role",
"Document.Kind",
"DIVISORS",
"COUNT",
"COMPONENT",
"CLASS",
"CHALLENGE",
"AnswerList",
"answer-list",
"Answer",
"ADJUSTMENT")) {
return new PropertyValidationRules(PropertyFilterType.LoincRelationship, CodeValidationRule.Error, addToOps(ops, PropertyOperation.Equals, PropertyOperation.RegEx, PropertyOperation.In, PropertyOperation.NotIn));
}
if (Utilities.existsInList(property,
"UNITSREQUIRED",
"PanelType",
"ORDER_OBS",
"EXAMPLE_UNITS",
"EXAMPLE_UCUM_UNITS",
"Copyright",
"CLASSTYPE",
"CLASS",
"AskAtOrderEntry")) {
return new PropertyValidationRules(PropertyFilterType.String, CodeValidationRule.None, addToOps(ops, PropertyOperation.Equals, PropertyOperation.RegEx, PropertyOperation.In, PropertyOperation.NotIn));
}
if (Utilities.existsInList(property,
"STATUS")) {
return new PropertyValidationRules(PropertyFilterType.CodeList, CodeValidationRule.None, addToOps(ops, PropertyOperation.Equals, PropertyOperation.RegEx, PropertyOperation.In, PropertyOperation.NotIn))
.setCodes("ACTIVE", "DEPRECATED", "DISCOURAGED", "DocumentOntology", "EXAMPLE", "NORMATIVE", "NotStated", "PREFERRED", "Primary", "Radiology", "TRIAL");
}
if (Utilities.existsInList(property,
"LIST",
"ancestor")) {
return new PropertyValidationRules(PropertyFilterType.Code, CodeValidationRule.None, addToOps(ops, PropertyOperation.Equals, PropertyOperation.RegEx, PropertyOperation.In, PropertyOperation.NotIn));
}
if (Utilities.existsInList(property,
"copyright")) {
return new PropertyValidationRules(PropertyFilterType.CodeList, CodeValidationRule.None, addToOps(ops, PropertyOperation.Equals, PropertyOperation.RegEx, PropertyOperation.In, PropertyOperation.NotIn)).setCodes("LOINC", "3rdParty");
}
if (Utilities.existsInList(property,
"concept")) {
return new PropertyValidationRules(PropertyFilterType.Code, CodeValidationRule.None, addToOps(ops, PropertyOperation.IsA));
}
return null;
}
private EnumSet<PropertyOperation> addToOps(EnumSet<PropertyOperation> set, PropertyOperation... ops) {
for (PropertyOperation op : ops) {
@ -648,7 +754,72 @@ public class ValueSetValidator extends BaseValidator {
private String[] getSystemKnownNames(String system) {
switch (system) {
case "http://loinc.org" : return new String[] {"parent", "ancestor", "copyright", "STATUS", "COMPONENT", "PROPERTY", "TIME_ASPCT", "SYSTEM", "SCALE_TYP", "METHOD_TYP", "CLASS", "CONSUMER_NAME", "CLASSTYPE", "ORDER_OBS", "DOCUMENT_SECTION"};
case "http://loinc.org" : return new String[] {
// = and regex
"parent",
"child",
"answers-for",
"TIME MODIFIER",
"TIME_ASPCT",
"SYSTEM",
"SUPER SYSTEM",
"SUFFIX",
"SCALE",
"SCALE_TYP",
"Rad.View.View Type",
"Rad.View.Aggregation",
"Rad.Timing",
"Rad.Subject",
"Rad.Reason for Exam",
"Rad.Pharmaceutical.Substance Given",
"Rad.Pharmaceutical.Route",
"Rad.Modality.Modality Type",
"Rad.Modality.Modality Subtype",
"Rad.Maneuver.Maneuver Type",
"Rad.Guidance for.Presence",
"Rad.Guidance for.Object",
"Rad.Guidance for.Approach",
"Rad.Guidance for.Action",
"Rad.Anatomic Location.Region Imaged",
"Rad.Anatomic Location.Laterality.Presence",
"Rad.Anatomic Location.Laterality",
"Rad.Anatomic Location.Imaging Focus",
"PROPERTY",
"METHOD",
"GENE",
"Document.TypeOfService",
"Document.SubjectMatterDomain",
"Document.Setting",
"Document.Role",
"Document.Kind",
"DIVISORS",
"COUNT",
"COMPONENT",
"CLASS",
"CHALLENGE",
"AnswerList",
"answer-list",
"Answer",
"ADJUSTMENT",
"UNITSREQUIRED",
"PanelType",
"ORDER_OBS",
"EXAMPLE_UNITS",
"EXAMPLE_UCUM_UNITS",
"Copyright",
"CLASSTYPE",
"CLASS",
"AskAtOrderEntry",
"ancestor",
// just equals
"STATUS",
"LIST",
"copyright",
// == is-a on:
"concept"};
case "http://snomed.info/sct": return new String[] { "constraint", "expressions", "410662002", "42752001", "47429007", "116676008", "116686009", "118168003", "118169006", "118170007", "118171006", "127489000", "131195008",
"246075003", "246090004", "246093002", "246112005", "246454002", "246456000", "246501002", "246513007", "246514001", "255234002", "260507000",