More valueset validation improvements after real world testing

This commit is contained in:
Grahame Grieve 2024-03-14 16:48:39 +11:00
parent 8897f956bf
commit e39bbba7ef
5 changed files with 101 additions and 60 deletions

View File

@ -932,6 +932,15 @@ public class Utilities {
return true;
return false;
}
public static boolean existsInListTrimmed(String value, String... array) {
if (value == null)
return false;
for (String s : array)
if (value.equals(s.trim()))
return true;
return false;
}
public static boolean existsInList(int value, int... array) {
for (int i : array)

View File

@ -300,8 +300,8 @@ public class VersionUtilities {
} else if (Utilities.charCount(version, '.') == 2) {
String[] p = version.split("\\.");
return p[0]+"."+p[1];
} else if (Utilities.existsInList(version, "R2", "R2B", "R3", "R4", "R4B", "R5", "R6")) {
switch (version) {
} else if (Utilities.existsInList(version.toUpperCase(), "R2", "R2B", "R3", "R4", "R4B", "R5", "R6")) {
switch (version.toUpperCase()) {
case "R2": return "1.0";
case "R2B": return "1.4";
case "R3": return "3.0";

View File

@ -1098,6 +1098,9 @@ public class I18nConstants {
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";
public static final String VALUESET_BAD_FILTER_VALUE_HAS_COMMA = "VALUESET_BAD_FILTER_VALUE_HAS_COMMA";
public static final String VALUESET_BAD_FILTER_VALUE_VALID_REGEX = "VALUESET_BAD_FILTER_VALUE_VALID_REGEX";
public static final String VALUESET_BAD_PROPERTY_NO_REGEX = "VALUESET_BAD_PROPERTY_NO_REGEX";
}

View File

@ -1128,7 +1128,7 @@ VALUESET_INCLUDE_CS_SUPPLEMENT = The value set references CodeSystem ''{0}'' whi
VALUESET_INCLUDE_CSVER_SUPPLEMENT = The value set references CodeSystem ''{0}'' version ''{2}'' which is a supplement. It must reference the underlying CodeSystem ''{1}'' and use the http://hl7.org/fhir/StructureDefinition/valueset-supplement extension for the supplement
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)
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, and/or talk to TSMG)
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}''
@ -1156,3 +1156,7 @@ VALUESET_BAD_FILTER_VALUE_VALID_CODE = The value for a filter based on property
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}
VALUESET_BAD_FILTER_VALUE_HAS_COMMA = The filter value has a comma, but the operation is different to 'in' and 'not-in', so the comma will be interpreted as part of the {0} value
VALUESET_BAD_FILTER_VALUE_VALID_REGEX = The value for a filter based on property ''{0}'' should be a valid regex, not ''{1}'' (err = ''{2}'')
VALUESET_BAD_PROPERTY_NO_REGEX = Cannot apply a regex filter to the property ''{0}'' (usually regex filters are applied to the codes, or a named property of the code system)

View File

@ -5,6 +5,8 @@ import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.hl7.fhir.r5.model.CodeSystem;
import org.hl7.fhir.r5.model.CodeSystem.CodeSystemFilterComponent;
@ -58,42 +60,47 @@ public class ValueSetValidator extends BaseValidator {
}
}
public enum CodeValidationRule {
Warning, Error, None
}
public class PropertyValidationRules {
boolean repeating;
PropertyFilterType type;
EnumSet<PropertyOperation> ops;
protected PropertyValidationRules(boolean repeating, PropertyFilterType type, PropertyOperation... ops) {
private PropertyFilterType type;
private CodeValidationRule codeValidation;
private EnumSet<PropertyOperation> ops;
protected PropertyValidationRules(PropertyFilterType type, CodeValidationRule codeValidation, PropertyOperation... ops) {
super();
this.repeating = repeating;
this.type = type;
this.codeValidation = codeValidation;
this.ops = EnumSet.noneOf(PropertyOperation.class);
for (PropertyOperation op : ops) {
this.ops.add(op);
}
}
public PropertyValidationRules(boolean repeating, PropertyFilterType type, EnumSet<PropertyOperation> ops) {
public PropertyValidationRules(PropertyFilterType type, CodeValidationRule codeValidation, EnumSet<PropertyOperation> ops) {
super();
this.repeating = repeating;
this.type = type;
this.codeValidation = codeValidation;
this.ops = ops;
}
public boolean isRepeating() {
return repeating;
}
public PropertyFilterType getType() {
return type;
}
public EnumSet<PropertyOperation> getOps() {
return ops;
}
public CodeValidationRule getCodeValidation() {
return codeValidation;
}
}
public enum PropertyFilterType {
Boolean, Integer, Decimal, Code, DateTime, ValidCode, Coding
Boolean, Integer, Decimal, Code, DateTime, Coding
}
private static final int TOO_MANY_CODES_TO_VALIDATE = 1000;
@ -422,13 +429,22 @@ public class ValueSetValidator extends BaseValidator {
}
if ("exists".equals(op)) {
ok = checkFilterValue(errors, stack, system, version, ok, property, value, PropertyFilterType.Boolean) && ok;
} else if (rules.isRepeating()) {
ok = checkFilterValue(errors, stack, system, version, ok, property, op, value, PropertyFilterType.Boolean, null) && ok;
} else if ("regex".equals(op)) {
String err = null;
try {
Pattern.compile(value);
} catch (PatternSyntaxException e) {
err = e.getMessage();
}
ok = rule(errors, "2024-03-09", IssueType.INVALID, stack, err == null, I18nConstants.VALUESET_BAD_FILTER_VALUE_VALID_REGEX, property, value, err) && ok;
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, v, rules.getType()) && ok;
ok = checkFilterValue(errors, stack, system, version, ok, property, op, v, rules.getType(), rules.getCodeValidation()) && ok;
}
} else {
ok = checkFilterValue(errors, stack, system, version, ok, property, value, rules.getType()) && ok;
ok = checkFilterValue(errors, stack, system, version, ok, property, op, value, rules.getType(), rules.getCodeValidation()) && ok;
}
}
}
@ -454,8 +470,11 @@ public class ValueSetValidator extends BaseValidator {
return false;
}
private boolean checkFilterValue(List<ValidationMessage> errors, NodeStack stack, String system, String version,boolean ok, String property, String value, PropertyFilterType type) {
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) {
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());
}
switch (type) {
case Boolean:
ok = rule(errors, "2024-03-09", IssueType.INVALID, stack,
@ -466,6 +485,14 @@ public class ValueSetValidator extends BaseValidator {
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) {
ValidationResult vr = context.validateCode(baseOptions, system, version, value, null);
if (cr == 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 DateTime:
ok = rule(errors, "2024-03-09", IssueType.INVALID, stack.getLiteralPath(),
@ -482,18 +509,12 @@ public class ValueSetValidator extends BaseValidator {
Utilities.isInteger(value),
I18nConstants.VALUESET_BAD_FILTER_VALUE_INTEGER, property, value) && ok;
break;
case ValidCode :
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, property, value, system, vr.getMessage()) && ok;
break;
case Coding :
Coding code = Coding.fromLiteral(value);
if (code == null) {
ok = rule(errors, "2024-03-09", IssueType.INVALID, stack, false, I18nConstants.VALUESET_BAD_FILTER_VALUE_CODED, property, value) && ok;
} else {
vr = context.validateCode(baseOptions, code, null);
ValidationResult vr = context.validateCode(baseOptions, code, null);
ok = rule(errors, "2024-03-09", IssueType.INVALID, stack, vr.isOk(), I18nConstants.VALUESET_BAD_FILTER_VALUE_CODED_INVALID, property, value, vr.getMessage()) && ok;
}
break;
@ -521,12 +542,16 @@ public class ValueSetValidator extends BaseValidator {
if (property.equals(p.getCode())) {
if (p.getType() != null) {
switch (p.getType()) {
case BOOLEAN: return new PropertyValidationRules(false, PropertyFilterType.Boolean, ops);
case CODE: return new PropertyValidationRules(false, PropertyFilterType.ValidCode, ops);
case CODING: return new PropertyValidationRules(false, PropertyFilterType.Coding, ops);
case DATETIME: return new PropertyValidationRules(false, PropertyFilterType.DateTime, ops);
case DECIMAL: return new PropertyValidationRules(false, PropertyFilterType.Decimal, ops);
case INTEGER: return new PropertyValidationRules(false, PropertyFilterType.Integer, ops);
case BOOLEAN: return new PropertyValidationRules(PropertyFilterType.Boolean, null, ops);
case CODE:
// the definitions say " a code that identifies a concept defined in the code system" -> ValidCode.
// but many people have ignored that and defined a property as 'code' because it's from a list of values that are otherwise undefined
boolean external = !forPublication || cs.getWebPath() == null || Utilities.isAbsoluteUrl(cs.getWebPath());
return new PropertyValidationRules(PropertyFilterType.Code, external ? CodeValidationRule.Warning : CodeValidationRule.Error, ops); // valid code... the definitions say that, but people were missing that in the pastm
case CODING: return new PropertyValidationRules(PropertyFilterType.Coding, null, ops);
case DATETIME: return new PropertyValidationRules(PropertyFilterType.DateTime, null, ops);
case DECIMAL: return new PropertyValidationRules(PropertyFilterType.Decimal, null, ops);
case INTEGER: return new PropertyValidationRules(PropertyFilterType.Integer, null, ops);
case STRING: return null;
}
}
@ -535,51 +560,51 @@ public class ValueSetValidator extends BaseValidator {
}
switch (property) {
case "concept" : return new PropertyValidationRules(false, PropertyFilterType.ValidCode, addToOps(ops, PropertyOperation.Equals, PropertyOperation.In, PropertyOperation.IsA, PropertyOperation.DescendentOf, PropertyOperation.DescendentLeaf, PropertyOperation.IsNotA, PropertyOperation.NotIn));
case "code" : return new PropertyValidationRules(false, PropertyFilterType.ValidCode, addToOps(ops, PropertyOperation.Equals, PropertyOperation.RegEx));
case "status" : return new PropertyValidationRules(false, PropertyFilterType.Code, ops);
case "inactive" : return new PropertyValidationRules(false, PropertyFilterType.Boolean, ops);
case "effectiveDate" : return new PropertyValidationRules(false, PropertyFilterType.DateTime, ops);
case "deprecationDate" : return new PropertyValidationRules(false, PropertyFilterType.DateTime, ops);
case "retirementDate" : return new PropertyValidationRules(false, PropertyFilterType.DateTime, ops);
case "notSelectable" : return new PropertyValidationRules(false, PropertyFilterType.Boolean, ops);
case "parent" : return new PropertyValidationRules(false, PropertyFilterType.ValidCode, ops);
case "child" : return new PropertyValidationRules(false, PropertyFilterType.ValidCode, ops);
case "partOf" : return new PropertyValidationRules(false, PropertyFilterType.ValidCode, ops);
case "synonym" : return new PropertyValidationRules(false, PropertyFilterType.Code, ops);
case "concept" : return new PropertyValidationRules(PropertyFilterType.Code, CodeValidationRule.Error, addToOps(ops, PropertyOperation.Equals, PropertyOperation.In, PropertyOperation.IsA, PropertyOperation.DescendentOf, PropertyOperation.DescendentLeaf, PropertyOperation.IsNotA, PropertyOperation.NotIn));
case "code" : return new PropertyValidationRules(PropertyFilterType.Code, CodeValidationRule.Error, addToOps(ops, PropertyOperation.Equals, PropertyOperation.RegEx));
case "status" : return new PropertyValidationRules(PropertyFilterType.Code, CodeValidationRule.None, ops);
case "inactive" : return new PropertyValidationRules(PropertyFilterType.Boolean,null, ops);
case "effectiveDate" : return new PropertyValidationRules(PropertyFilterType.DateTime, null, ops);
case "deprecationDate" : return new PropertyValidationRules(PropertyFilterType.DateTime, null, ops);
case "retirementDate" : return new PropertyValidationRules(PropertyFilterType.DateTime, null, ops);
case "notSelectable" : return new PropertyValidationRules(PropertyFilterType.Boolean, null, ops);
case "parent" : return new PropertyValidationRules(PropertyFilterType.Code, CodeValidationRule.Error, ops);
case "child" : return new PropertyValidationRules(PropertyFilterType.Code, CodeValidationRule.Error, ops);
case "partOf" : return new PropertyValidationRules(PropertyFilterType.Code, CodeValidationRule.Error, ops);
case "synonym" : return new PropertyValidationRules(PropertyFilterType.Code, CodeValidationRule.None, ops); // ? none?
case "comment" : return null;
case "itemWeight" : return new PropertyValidationRules(false, PropertyFilterType.Decimal, ops);
case "itemWeight" : return new PropertyValidationRules(PropertyFilterType.Decimal, null, ops);
}
switch (system) {
case "http://loinc.org" :
if (Utilities.existsInList(property, "copyright", "STATUS", "CLASS", "CONSUMER_NAME", "ORDER_OBS", "DOCUMENT_SECTION")) {
return new PropertyValidationRules(false, PropertyFilterType.Code);
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(false, PropertyFilterType.Integer, addToOps(ops, PropertyOperation.Equals, PropertyOperation.In));
return new PropertyValidationRules(PropertyFilterType.Integer, null, addToOps(ops, PropertyOperation.Equals, PropertyOperation.In));
} else {
return new PropertyValidationRules(false, PropertyFilterType.ValidCode, addToOps(ops, PropertyOperation.Equals, PropertyOperation.In));
return new PropertyValidationRules(PropertyFilterType.Code, CodeValidationRule.Error, addToOps(ops, PropertyOperation.Equals, PropertyOperation.In));
}
case "http://snomed.info/sct":
switch (property) {
case "constraint": return null; // for now
case "expressions": return new PropertyValidationRules(false, PropertyFilterType.Boolean, addToOps(ops, PropertyOperation.Equals, PropertyOperation.In));
case "expressions": return new PropertyValidationRules(PropertyFilterType.Boolean, null, addToOps(ops, PropertyOperation.Equals, PropertyOperation.In));
default:
return new PropertyValidationRules(false, PropertyFilterType.ValidCode, addToOps(ops, PropertyOperation.Equals, PropertyOperation.In));
return new PropertyValidationRules(PropertyFilterType.Code, CodeValidationRule.Error, addToOps(ops, PropertyOperation.Equals, PropertyOperation.In));
}
case "http://www.nlm.nih.gov/research/umls/rxnorm" : return new PropertyValidationRules(false, PropertyFilterType.Code, ops);
case "http://unitsofmeasure.org" : return new PropertyValidationRules(false, PropertyFilterType.Code, ops);
case "http://www.nlm.nih.gov/research/umls/rxnorm" : return new PropertyValidationRules(PropertyFilterType.Code, CodeValidationRule.None, ops);
case "http://unitsofmeasure.org" : return new PropertyValidationRules(PropertyFilterType.Code, CodeValidationRule.None, ops);
case "http://www.ama-assn.org/go/cpt" :
switch (property) {
case "modifier": return new PropertyValidationRules(false, PropertyFilterType.Boolean, ops);
case "kind" : return new PropertyValidationRules(true, PropertyFilterType.Code, ops); // for now
case "modified": return new PropertyValidationRules(false, PropertyFilterType.Boolean, ops);
case "modifier": return new PropertyValidationRules(PropertyFilterType.Boolean, null, ops);
case "kind" : return new PropertyValidationRules(PropertyFilterType.Code, CodeValidationRule.None, ops); // for now
case "modified": return new PropertyValidationRules(PropertyFilterType.Boolean, null, ops);
case "code" : return null;
case "telemedicine": return new PropertyValidationRules(false, PropertyFilterType.Boolean, ops);
case "orthopox" : return new PropertyValidationRules(false, PropertyFilterType.Boolean, ops);
case "telemedicine": return new PropertyValidationRules(PropertyFilterType.Boolean, null, ops);
case "orthopox" : return new PropertyValidationRules(PropertyFilterType.Boolean,null, ops);
}
}
if (ops != null) {
return new PropertyValidationRules(false, null, ops);
return new PropertyValidationRules(null, null, ops);
} else {
return null;
}