ValueSetValidation improvements

This commit is contained in:
Grahame Grieve 2024-03-11 00:19:16 +11:00
parent 33a1814d4e
commit 9e8a010c13
12 changed files with 757 additions and 331 deletions

View File

@ -563,7 +563,26 @@ public class Coding extends DataType implements IBaseCoding, ICompositeType, ICo
if (hasDisplay()) if (hasDisplay())
base = base+": '"+getDisplay()+"'"; base = base+": '"+getDisplay()+"'";
return base; return base;
}
public static Coding fromLiteral(String value) {
String sv = value.contains("#") ? value.substring(0, value.indexOf("#")) : value;
String cp = value.contains("#") ? value.substring(value.indexOf("#")+1) : null;
String system = sv.contains("|") ? sv.substring(0, sv.indexOf("|")) : sv;
String version = sv.contains("|") ? sv.substring(sv.indexOf("|")+1) : null;
String code = cp != null && cp.contains("'") ? cp.substring(0, cp.indexOf("'")) : cp;
String display = cp != null && cp.contains("'") ? cp.substring(cp.indexOf("'")+1) : null;
if (display != null) {
display = display.trim();
display = display.substring(0, display.length() -1);
}
if ((system == null || !Utilities.isAbsoluteUrl(system)) && code == null) {
return null;
} else {
return new Coding(system, version, code, display);
}
} }
public boolean matches(Coding other) { public boolean matches(Coding other) {

View File

@ -1056,6 +1056,8 @@ public class CodeSystemUtilities extends TerminologyUtilities {
return null; return null;
} }
public static boolean isExemptFromMultipleVersionChecking(String url) {
return Utilities.existsInList(url, "http://snomed.info/sct", "http://loinc.org");
}
} }

View File

@ -1,8 +1,11 @@
package org.hl7.fhir.utilities; package org.hl7.fhir.utilities;
import java.util.Collection; import java.util.Collection;
import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
@ -171,4 +174,12 @@ public class CommaSeparatedStringBuilder {
} }
return b.toString(); return b.toString();
} }
public static String join(String sep, EnumSet<? extends Enum> set) {
CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(sep);
for (Enum e : set) {
b.append(e.toString());
}
return b.toString();
}
} }

View File

@ -1075,7 +1075,7 @@ public class I18nConstants {
public static final String TYPE_SPECIFIC_CHECKS_DT_CANONICAL_MULTIPLE_POSSIBLE_VERSIONS = "TYPE_SPECIFIC_CHECKS_DT_CANONICAL_MULTIPLE_POSSIBLE_VERSIONS"; 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_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_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_SYNONYM_CHECK = "CODESYSTEM_PROPERTY_SYNONYM_CHECK";
public static final String CODESYSTEM_PROPERTY_DUPLICATE_CODE = "CODESYSTEM_PROPERTY_DUPLICATE_CODE"; 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_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_URI_TYPE_MISMATCH = "CODESYSTEM_PROPERTY_URI_TYPE_MISMATCH";
@ -1087,6 +1087,17 @@ public class I18nConstants {
public static final String CODESYSTEM_PROPERTY_WRONG_TYPE = "CODESYSTEM_PROPERTY_WRONG_TYPE"; 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_NO_LANG = "CODESYSTEM_DESIGNATION_DISP_CLASH_NO_LANG";
public static final String CODESYSTEM_DESIGNATION_DISP_CLASH_LANG = "CODESYSTEM_DESIGNATION_DISP_CLASH_LANG"; public static final String CODESYSTEM_DESIGNATION_DISP_CLASH_LANG = "CODESYSTEM_DESIGNATION_DISP_CLASH_LANG";
public static final String VALUESET_UNKNOWN_FILTER_PROPERTY_NO_CS = "VALUESET_UNKNOWN_FILTER_PROPERTY_NO_CS";
public static final String VALUESET_UNKNOWN_FILTER_PROPERTY = "VALUESET_UNKNOWN_FILTER_PROPERTY";
public static final String VALUESET_BAD_FILTER_VALUE_BOOLEAN = "VALUESET_BAD_FILTER_VALUE_BOOLEAN";
public static final String VALUESET_BAD_FILTER_VALUE_CODE = "VALUESET_BAD_FILTER_VALUE_CODE";
public static final String VALUESET_BAD_FILTER_VALUE_DATETIME = "VALUESET_BAD_FILTER_VALUE_DATETIME";
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_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

@ -1133,15 +1133,26 @@ TYPE_SPECIFIC_CHECKS_DT_CANONICAL_MULTIPLE_POSSIBLE_VERSIONS = There are multipl
ABSTRACT_CODE_NOT_ALLOWED = Code ''{0}#{1}'' is abstract, and not allowed in this context 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_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_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_SYNONYM_CHECK = The synonym ''{0}'' is not also defined in the code system. The Synonym property should only used to declare equivalence to other existing codes
CODESYSTEM_PROPERTY_DUPLICATE_CODE = A property is already defined with the code ''{0}'' 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_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_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_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_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_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_UNDEFINED = The property ''{0}'' has no definition in CodeSystem.property. 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_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_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_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}'') 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}'')
VALUESET_UNKNOWN_FILTER_PROPERTY = The property ''{0}'' is not known for the system ''{1}'', so may not be understood by the terminology ecosystem. Known properties for this system: {2}
VALUESET_UNKNOWN_FILTER_PROPERTY_NO_CS = No definition can be found for the system {1}, and the property ''{0}'' is not a generally known property, so the property might not be valid, or understood by the terminology ecosystem. In case it's useful, the list of generally known properties for all systems is {2}
VALUESET_BAD_FILTER_VALUE_BOOLEAN = The value for a filter based on property ''{0}'' must be either ''true'' or ''false'', not ''{1}''
VALUESET_BAD_FILTER_VALUE_CODE = The value for a filter based on property ''{0}'' must be a valid code, not ''{1}''
VALUESET_BAD_FILTER_VALUE_DATETIME = The value for a filter based on property ''{0}'' must be a valid date(/time), not ''{1}''
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_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

@ -1241,7 +1241,8 @@ public class ValidationEngine implements IValidatorResourceFetcher, IValidationP
Set<String> res = new HashSet<>(); Set<String> res = new HashSet<>();
for (Resource r : context.fetchResourcesByUrl(Resource.class, url)) { for (Resource r : context.fetchResourcesByUrl(Resource.class, url)) {
if (r instanceof CanonicalResource) { if (r instanceof CanonicalResource) {
res.add(((CanonicalResource) r).getVersion()); CanonicalResource cr = (CanonicalResource) r;
res.add(cr.hasVersion() ? cr.getVersion() : "{{unversioned}}");
} }
} }
if (fetcher != null) { if (fetcher != null) {

View File

@ -5724,6 +5724,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
String pub = element.getNamedChildValue("publisher", false); String pub = element.getNamedChildValue("publisher", false);
Base wgT = element.getExtensionValue(ToolingExtensions.EXT_WORKGROUP); Base wgT = element.getExtensionValue(ToolingExtensions.EXT_WORKGROUP);
String wg = wgT == null ? null : wgT.primitiveValue(); String wg = wgT == null ? null : wgT.primitiveValue();
String url = element.getNamedChildValue("url");
if (contained && wg == null) { if (contained && wg == null) {
boolean ok = true; boolean ok = true;
@ -5750,7 +5751,6 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
} }
} }
List<String> urls = new ArrayList<>(); List<String> urls = new ArrayList<>();
for (Element c : element.getChildren("contact")) { for (Element c : element.getChildren("contact")) {
for (Element t : c.getChildren("telecom")) { for (Element t : c.getChildren("telecom")) {
@ -5760,7 +5760,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
} }
} }
if (rule(errors, "2023-09-15", IssueType.BUSINESSRULE, element.line(), element.col(), stack.getLiteralPath(), wg != null, I18nConstants.VALIDATION_HL7_WG_NEEDED, ToolingExtensions.EXT_WORKGROUP)) { if (rule(errors, "2023-09-15", IssueType.BUSINESSRULE, element.line(), element.col(), stack.getLiteralPath(), wg != null && !url.contains("http://hl7.org/fhir/sid"), I18nConstants.VALIDATION_HL7_WG_NEEDED, ToolingExtensions.EXT_WORKGROUP)) {
HL7WorkGroup wgd = HL7WorkGroups.find(wg); HL7WorkGroup wgd = HL7WorkGroups.find(wg);
if (rule(errors, "2023-09-15", IssueType.BUSINESSRULE, element.line(), element.col(), stack.getLiteralPath(), wgd != null, I18nConstants.VALIDATION_HL7_WG_UNKNOWN, wg)) { if (rule(errors, "2023-09-15", IssueType.BUSINESSRULE, element.line(), element.col(), stack.getLiteralPath(), wgd != null, I18nConstants.VALIDATION_HL7_WG_UNKNOWN, wg)) {
String rpub = "HL7 International / "+wgd.getName(); String rpub = "HL7 International / "+wgd.getName();

View File

@ -188,6 +188,11 @@ public class CodeSystemValidator extends BaseValidator {
ok = checkConcept(errors, cs, stack.push(concept, i, null, null), "true".equals(caseSensitive), hierarchyMeaning, csB, concept, codes, properties) && ok; ok = checkConcept(errors, cs, stack.push(concept, i, null, null), "true".equals(caseSensitive), hierarchyMeaning, csB, concept, codes, properties) && ok;
i++; i++;
} }
i = 0;
for (Element concept : concepts) {
ok = checkConceptProps(errors, cs, stack.push(concept, i, null, null), "true".equals(caseSensitive), hierarchyMeaning, csB, concept, codes, properties) && ok;
i++;
}
return ok; return ok;
} }
@ -250,7 +255,6 @@ public class CodeSystemValidator extends BaseValidator {
ok = false; ok = false;
rule(errors, "2024-03-06", IssueType.BUSINESSRULE, cs.line(), cs.col(), stack.getLiteralPath(), false, I18nConstants.CODESYSTEM_PROPERTY_BAD_HL7_URI, uri); 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 (code != null) {
@ -336,19 +340,8 @@ 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> propertyElements = concept.getChildrenByName("property");
int i = 0;
for (Element propertyElement : propertyElements) {
ok = checkPropertyValue(errors, cs, stack.push(propertyElement, i, null, null), propertyElement, properties) && ok;
i++;
}
List<Element> designations = concept.getChildrenByName("designation"); List<Element> designations = concept.getChildrenByName("designation");
i = 0; int i = 0;
for (Element designation : designations) { for (Element designation : designations) {
ok = checkDesignation(errors, cs, stack.push(designation, i, null, null), concept, designation) && ok; ok = checkDesignation(errors, cs, stack.push(designation, i, null, null), concept, designation) && ok;
i++; i++;
@ -363,6 +356,25 @@ public class CodeSystemValidator extends BaseValidator {
return ok; return ok;
} }
private boolean checkConceptProps(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;
List<Element> propertyElements = concept.getChildrenByName("property");
int i = 0;
for (Element propertyElement : propertyElements) {
ok = checkPropertyValue(errors, cs, stack.push(propertyElement, i, null, null), propertyElement, properties, codes) && ok;
i++;
}
List<Element> concepts = concept.getChildrenByName("concept");
i = 0;
for (Element child : concepts) {
ok = checkConceptProps(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) { private boolean checkDesignation(List<ValidationMessage> errors, Element cs, NodeStack stack, Element concept, Element designation) {
boolean ok = true; boolean ok = true;
@ -391,7 +403,7 @@ public class CodeSystemValidator extends BaseValidator {
return ok; return ok;
} }
private boolean checkPropertyValue(List<ValidationMessage> errors, Element cs, NodeStack stack, Element property, Map<String, PropertyDef> properties) { private boolean checkPropertyValue(List<ValidationMessage> errors, Element cs, NodeStack stack, Element property, Map<String, PropertyDef> properties, Set<String> codes) {
boolean ok = true; boolean ok = true;
String code = property.getNamedChildValue("code"); String code = property.getNamedChildValue("code");
@ -405,6 +417,10 @@ public class CodeSystemValidator extends BaseValidator {
} else { } else {
ok = false; ok = false;
} }
if ("synonym".equals(code)) {
String vcode = value.isPrimitive() ? value.primitiveValue() : null;
warning(errors, "2024-03-06", IssueType.BUSINESSRULE, cs.line(), cs.col(), stack.getLiteralPath(), codes.contains(vcode), I18nConstants.CODESYSTEM_PROPERTY_SYNONYM_CHECK, vcode);
}
} }
return ok; return ok;
} }

View File

@ -232,7 +232,7 @@ public class ConceptMapValidator extends BaseValidator {
} else { } else {
warning(errors, "2023-03-05", IssueType.NOTFOUND, grp.line(), grp.col(), stack.push(e, -1, null, null).getLiteralPath(), sourceScope != null, I18nConstants.CONCEPTMAP_GROUP_SOURCE_UNKNOWN, e.getValue()); warning(errors, "2023-03-05", IssueType.NOTFOUND, grp.line(), grp.col(), stack.push(e, -1, null, null).getLiteralPath(), sourceScope != null, I18nConstants.CONCEPTMAP_GROUP_SOURCE_UNKNOWN, e.getValue());
} }
if (ctxt.source.version == null && ctxt.source.cs != null) { if (ctxt.source.version == null && ctxt.source.cs != null && !CodeSystemUtilities.isExemptFromMultipleVersionChecking(ctxt.source.url)) {
Set<String> possibleVersions = fetcher.fetchCanonicalResourceVersions(null, valContext.getAppContext(), ctxt.source.url); Set<String> possibleVersions = fetcher.fetchCanonicalResourceVersions(null, valContext.getAppContext(), ctxt.source.url);
warning(errors, NO_RULE_DATE, IssueType.INVALID, grp.line(), grp.col(), stack.getLiteralPath(), possibleVersions.size() <= 1, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_CANONICAL_MULTIPLE_POSSIBLE_VERSIONS, warning(errors, NO_RULE_DATE, IssueType.INVALID, grp.line(), grp.col(), stack.getLiteralPath(), possibleVersions.size() <= 1, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_CANONICAL_MULTIPLE_POSSIBLE_VERSIONS,
ctxt.source.url, ctxt.source.cs.getVersion(), CommaSeparatedStringBuilder.join(", ", Utilities.sorted(possibleVersions))); ctxt.source.url, ctxt.source.cs.getVersion(), CommaSeparatedStringBuilder.join(", ", Utilities.sorted(possibleVersions)));
@ -249,7 +249,7 @@ public class ConceptMapValidator extends BaseValidator {
warning(errors, "2023-03-05", IssueType.NOTFOUND, grp.line(), grp.col(), stack.push(e, -1, null, null).getLiteralPath(), targetScope != null, I18nConstants.CONCEPTMAP_GROUP_TARGET_UNKNOWN, e.getValue()); warning(errors, "2023-03-05", IssueType.NOTFOUND, grp.line(), grp.col(), stack.push(e, -1, null, null).getLiteralPath(), targetScope != null, I18nConstants.CONCEPTMAP_GROUP_TARGET_UNKNOWN, e.getValue());
} }
if (ctxt.target.version == null && ctxt.target.cs != null) { if (ctxt.target.version == null && ctxt.target.cs != null && !CodeSystemUtilities.isExemptFromMultipleVersionChecking(ctxt.target.url)) {
Set<String> possibleVersions = fetcher.fetchCanonicalResourceVersions(null, valContext.getAppContext(), ctxt.target.url); Set<String> possibleVersions = fetcher.fetchCanonicalResourceVersions(null, valContext.getAppContext(), ctxt.target.url);
warning(errors, NO_RULE_DATE, IssueType.INVALID, grp.line(), grp.col(), stack.getLiteralPath(), possibleVersions.size() <= 1, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_CANONICAL_MULTIPLE_POSSIBLE_VERSIONS, warning(errors, NO_RULE_DATE, IssueType.INVALID, grp.line(), grp.col(), stack.getLiteralPath(), possibleVersions.size() <= 1, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_CANONICAL_MULTIPLE_POSSIBLE_VERSIONS,
ctxt.target.url, ctxt.target.cs.getVersion(), CommaSeparatedStringBuilder.join(", ", Utilities.sorted(possibleVersions))); ctxt.target.url, ctxt.target.cs.getVersion(), CommaSeparatedStringBuilder.join(", ", Utilities.sorted(possibleVersions)));

View File

@ -1,14 +1,21 @@
package org.hl7.fhir.validation.instance.type; package org.hl7.fhir.validation.instance.type;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import org.hl7.fhir.r5.model.CodeSystem; import org.hl7.fhir.r5.model.CodeSystem;
import org.hl7.fhir.r5.model.CodeSystem.CodeSystemFilterComponent;
import org.hl7.fhir.r5.model.CodeSystem.PropertyComponent;
import org.hl7.fhir.r5.elementmodel.Element; import org.hl7.fhir.r5.elementmodel.Element;
import org.hl7.fhir.r5.model.Coding; import org.hl7.fhir.r5.model.Coding;
import org.hl7.fhir.r5.model.Enumeration;
import org.hl7.fhir.r5.model.Enumerations.FilterOperator;
import org.hl7.fhir.r5.model.Resource; import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.r5.model.ValueSet;
import org.hl7.fhir.r5.terminologies.CodeSystemUtilities;
import org.hl7.fhir.r5.terminologies.utilities.CodingValidationRequest; import org.hl7.fhir.r5.terminologies.utilities.CodingValidationRequest;
import org.hl7.fhir.r5.terminologies.utilities.TerminologyServiceErrorClass; import org.hl7.fhir.r5.terminologies.utilities.TerminologyServiceErrorClass;
import org.hl7.fhir.r5.terminologies.utilities.ValidationResult; import org.hl7.fhir.r5.terminologies.utilities.ValidationResult;
@ -24,10 +31,70 @@ import org.hl7.fhir.validation.codesystem.CodeSystemChecker;
import org.hl7.fhir.validation.codesystem.GeneralCodeSystemChecker; import org.hl7.fhir.validation.codesystem.GeneralCodeSystemChecker;
import org.hl7.fhir.validation.codesystem.SnomedCTChecker; import org.hl7.fhir.validation.codesystem.SnomedCTChecker;
import org.hl7.fhir.validation.instance.InstanceValidator; import org.hl7.fhir.validation.instance.InstanceValidator;
import org.hl7.fhir.validation.instance.type.ValueSetValidator.PropertyOperation;
import org.hl7.fhir.validation.instance.type.ValueSetValidator.PropertyValidationRules;
import org.hl7.fhir.validation.instance.utils.NodeStack; import org.hl7.fhir.validation.instance.utils.NodeStack;
import org.hl7.fhir.validation.instance.utils.ValidationContext; import org.hl7.fhir.validation.instance.utils.ValidationContext;
public class ValueSetValidator extends BaseValidator { public class ValueSetValidator extends BaseValidator {
public enum PropertyOperation {
Equals, IsA, DescendentOf, IsNotA, RegEx, In, NotIn, Generalizes, ChildOf, DescendentLeaf, Exists;
public String toString() {
switch (this) {
case ChildOf: return "child-of";
case DescendentLeaf: return "descendent-leaf";
case DescendentOf: return "descendent-of";
case Equals: return "=";
case Exists: return "exists";
case Generalizes: return "generalizes";
case In: return "in";
case IsA: return "is-a";
case IsNotA: return "is-not-a";
case NotIn: return "not-in";
case RegEx: return "regex";
default: return "?";
}
}
}
public class PropertyValidationRules {
boolean repeating;
PropertyFilterType type;
EnumSet<PropertyOperation> ops;
protected PropertyValidationRules(boolean repeating, PropertyFilterType type, PropertyOperation... ops) {
super();
this.repeating = repeating;
this.type = type;
this.ops = EnumSet.noneOf(PropertyOperation.class);
for (PropertyOperation op : ops) {
this.ops.add(op);
}
}
public PropertyValidationRules(boolean repeating, PropertyFilterType type, EnumSet<PropertyOperation> ops) {
super();
this.repeating = repeating;
this.type = type;
this.ops = ops;
}
public boolean isRepeating() {
return repeating;
}
public PropertyFilterType getType() {
return type;
}
public EnumSet<PropertyOperation> getOps() {
return ops;
}
}
public enum PropertyFilterType {
Boolean, Integer, Decimal, Code, DateTime, ValidCode, Coding
}
private static final int TOO_MANY_CODES_TO_VALIDATE = 1000; private static final int TOO_MANY_CODES_TO_VALIDATE = 1000;
@ -80,22 +147,22 @@ public class ValueSetValidator extends BaseValidator {
if (parent.isForPublication()) { if (parent.isForPublication()) {
if (isHL7(vs)) { if (isHL7(vs)) {
boolean ok = true; boolean ok = true;
ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("url", false), I18nConstants.VALUESET_SHAREABLE_MISSING_HL7, "url") && ok; ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, stack, vs.hasChild("url", false), I18nConstants.VALUESET_SHAREABLE_MISSING_HL7, "url") && ok;
ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("version", false), I18nConstants.VALUESET_SHAREABLE_MISSING_HL7, "version") && ok; ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, stack, vs.hasChild("version", false), I18nConstants.VALUESET_SHAREABLE_MISSING_HL7, "version") && ok;
ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("title", false), I18nConstants.VALUESET_SHAREABLE_MISSING_HL7, "title") && ok; ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, stack, vs.hasChild("title", false), I18nConstants.VALUESET_SHAREABLE_MISSING_HL7, "title") && ok;
warning(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("name", false), I18nConstants.VALUESET_SHAREABLE_EXTRA_MISSING_HL7, "name"); warning(errors, NO_RULE_DATE, IssueType.REQUIRED, stack, vs.hasChild("name", false), I18nConstants.VALUESET_SHAREABLE_EXTRA_MISSING_HL7, "name");
ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("status", false), I18nConstants.VALUESET_SHAREABLE_MISSING_HL7, "status") && ok; ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, stack, vs.hasChild("status", false), I18nConstants.VALUESET_SHAREABLE_MISSING_HL7, "status") && ok;
ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("experimental", false), I18nConstants.VALUESET_SHAREABLE_MISSING_HL7, "experimental") && ok; ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, stack, vs.hasChild("experimental", false), I18nConstants.VALUESET_SHAREABLE_MISSING_HL7, "experimental") && ok;
ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("description", false), I18nConstants.VALUESET_SHAREABLE_MISSING_HL7, "description") && ok; ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, stack, vs.hasChild("description", false), I18nConstants.VALUESET_SHAREABLE_MISSING_HL7, "description") && ok;
return ok; return ok;
} else { } else {
warning(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("url", false), I18nConstants.VALUESET_SHAREABLE_MISSING, "url"); warning(errors, NO_RULE_DATE, IssueType.REQUIRED, stack, vs.hasChild("url", false), I18nConstants.VALUESET_SHAREABLE_MISSING, "url");
warning(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("version", false), I18nConstants.VALUESET_SHAREABLE_MISSING, "version"); warning(errors, NO_RULE_DATE, IssueType.REQUIRED, stack, vs.hasChild("version", false), I18nConstants.VALUESET_SHAREABLE_MISSING, "version");
warning(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("title", false), I18nConstants.VALUESET_SHAREABLE_MISSING, "title"); warning(errors, NO_RULE_DATE, IssueType.REQUIRED, stack, vs.hasChild("title", false), I18nConstants.VALUESET_SHAREABLE_MISSING, "title");
warning(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("name", false), I18nConstants.VALUESET_SHAREABLE_EXTRA_MISSING, "name"); warning(errors, NO_RULE_DATE, IssueType.REQUIRED, stack, vs.hasChild("name", false), I18nConstants.VALUESET_SHAREABLE_EXTRA_MISSING, "name");
warning(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("status", false), I18nConstants.VALUESET_SHAREABLE_MISSING, "status"); warning(errors, NO_RULE_DATE, IssueType.REQUIRED, stack, vs.hasChild("status", false), I18nConstants.VALUESET_SHAREABLE_MISSING, "status");
warning(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("experimental", false), I18nConstants.VALUESET_SHAREABLE_MISSING, "experimental"); warning(errors, NO_RULE_DATE, IssueType.REQUIRED, stack, vs.hasChild("experimental", false), I18nConstants.VALUESET_SHAREABLE_MISSING, "experimental");
warning(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("description", false), I18nConstants.VALUESET_SHAREABLE_MISSING, "description"); warning(errors, NO_RULE_DATE, IssueType.REQUIRED, stack, vs.hasChild("description", false), I18nConstants.VALUESET_SHAREABLE_MISSING, "description");
} }
} }
return true; return true;
@ -143,17 +210,17 @@ public class ValueSetValidator extends BaseValidator {
i++; i++;
} }
if (valuesets.size() > 1) { if (valuesets.size() > 1) {
warning(errors, NO_RULE_DATE, IssueType.INFORMATIONAL, stack.getLiteralPath(), false, I18nConstants.VALUESET_IMPORT_UNION_INTERSECTION); warning(errors, NO_RULE_DATE, IssueType.INFORMATIONAL, stack, false, I18nConstants.VALUESET_IMPORT_UNION_INTERSECTION);
} }
if (system != null) { if (system != null) {
rule(errors, "2024-03-06", IssueType.INVALID, stack.getLiteralPath(), Utilities.isAbsoluteUrl(system), system.startsWith("#") ? I18nConstants.VALUESET_INCLUDE_SYSTEM_ABSOLUTE_FRAG : I18nConstants.VALUESET_INCLUDE_SYSTEM_ABSOLUTE, system); rule(errors, "2024-03-06", IssueType.INVALID, stack, Utilities.isAbsoluteUrl(system), system.startsWith("#") ? I18nConstants.VALUESET_INCLUDE_SYSTEM_ABSOLUTE_FRAG : I18nConstants.VALUESET_INCLUDE_SYSTEM_ABSOLUTE, system);
if (system.startsWith("#")) { if (system.startsWith("#")) {
List<Element> cs = new ArrayList<>(); List<Element> cs = new ArrayList<>();
for (Element contained : vsSrc.getChildrenByName("contained")) { for (Element contained : vsSrc.getChildrenByName("contained")) {
if (("#"+contained.getIdBase()).equals(system)) { if (("#"+contained.getIdBase()).equals(system)) {
ok = false; // see absolute check above. ok = false; // see absolute check above.
if (rule(errors, "2024-02-10", IssueType.INVALID, stack.getLiteralPath(), "CodeSystem".equals(contained.fhirType()), I18nConstants.VALUESET_INCLUDE_CS_NOT_CS, system, contained.fhirType())) { if (rule(errors, "2024-02-10", IssueType.INVALID, stack, "CodeSystem".equals(contained.fhirType()), I18nConstants.VALUESET_INCLUDE_CS_NOT_CS, system, contained.fhirType())) {
if (version == null || version.equals(contained.getChildValue("version"))) { if (version == null || version.equals(contained.getChildValue("version"))) {
cs.add(contained); cs.add(contained);
} }
@ -161,16 +228,16 @@ public class ValueSetValidator extends BaseValidator {
} }
} }
if (cs.isEmpty()) { if (cs.isEmpty()) {
ok = rule(errors, "2024-02-10", IssueType.INVALID, stack.getLiteralPath(), false, version == null ? I18nConstants.VALUESET_INCLUDE_CS_NOT_FOUND : I18nConstants.VALUESET_INCLUDE_CSVER_NOT_FOUND, system, version) && ok; ok = rule(errors, "2024-02-10", IssueType.INVALID, stack, false, version == null ? I18nConstants.VALUESET_INCLUDE_CS_NOT_FOUND : I18nConstants.VALUESET_INCLUDE_CSVER_NOT_FOUND, system, version) && ok;
} else { } else {
ok = rule(errors, "2024-02-10", IssueType.INVALID, stack.getLiteralPath(), cs.size() == 1, version == null ? I18nConstants.VALUESET_INCLUDE_CS_MULTI_FOUND : I18nConstants.VALUESET_INCLUDE_CSVER_MULTI_FOUND, system, version) && ok; ok = rule(errors, "2024-02-10", IssueType.INVALID, stack, cs.size() == 1, version == null ? I18nConstants.VALUESET_INCLUDE_CS_MULTI_FOUND : I18nConstants.VALUESET_INCLUDE_CSVER_MULTI_FOUND, system, version) && ok;
} }
} }
if (version == null) { if (version == null) {
CodeSystem cs = context.fetchCodeSystem(system); CodeSystem cs = context.fetchCodeSystem(system);
if (cs != null) { if (cs != null && !CodeSystemUtilities.isExemptFromMultipleVersionChecking(system)) {
Set<String> possibleVersions = fetcher.fetchCanonicalResourceVersions(null, valContext.getAppContext(), system); Set<String> possibleVersions = fetcher.fetchCanonicalResourceVersions(null, valContext.getAppContext(), system);
warning(errors, NO_RULE_DATE, IssueType.INVALID, include.line(), include.col(), stack.getLiteralPath(), possibleVersions.size() <= 1, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_CANONICAL_MULTIPLE_POSSIBLE_VERSIONS, warning(errors, NO_RULE_DATE, IssueType.INVALID, stack, possibleVersions.size() <= 1, I18nConstants.TYPE_SPECIFIC_CHECKS_DT_CANONICAL_MULTIPLE_POSSIBLE_VERSIONS,
system, cs.getVersion(), CommaSeparatedStringBuilder.join(", ", Utilities.sorted(possibleVersions))); system, cs.getVersion(), CommaSeparatedStringBuilder.join(", ", Utilities.sorted(possibleVersions)));
} }
} }
@ -179,18 +246,19 @@ public class ValueSetValidator extends BaseValidator {
List<Element> filters = include.getChildrenByName("filter"); List<Element> filters = include.getChildrenByName("filter");
CodeSystemChecker slv = getSystemValidator(system, errors); CodeSystemChecker slv = getSystemValidator(system, errors);
CodeSystem cs = null;
if (!Utilities.noString(system)) { if (!Utilities.noString(system)) {
CodeSystem cs = context.fetchCodeSystem(system, version); cs = context.fetchCodeSystem(system, version);
if (cs != null) { // if it's null, we can't analyse this if (cs != null) { // if it's null, we can't analyse this
switch (cs.getContent()) { switch (cs.getContent()) {
case EXAMPLE: case EXAMPLE:
warning(errors, "2024-03-06", IssueType.INVALID, stack.getLiteralPath(), false, version == null ? I18nConstants.VALUESET_INCLUDE_CS_CONTENT : I18nConstants.VALUESET_INCLUDE_CSVER_CONTENT, system, cs.getContent().toCode(), version); warning(errors, "2024-03-06", IssueType.INVALID, stack, false, version == null ? I18nConstants.VALUESET_INCLUDE_CS_CONTENT : I18nConstants.VALUESET_INCLUDE_CSVER_CONTENT, system, cs.getContent().toCode(), version);
break; break;
case FRAGMENT: case FRAGMENT:
hint(errors, "2024-03-06", IssueType.INVALID, stack.getLiteralPath(), false, version == null ? I18nConstants.VALUESET_INCLUDE_CS_CONTENT : I18nConstants.VALUESET_INCLUDE_CSVER_CONTENT, system, cs.getContent().toCode(), version); hint(errors, "2024-03-06", IssueType.INVALID, stack, false, version == null ? I18nConstants.VALUESET_INCLUDE_CS_CONTENT : I18nConstants.VALUESET_INCLUDE_CSVER_CONTENT, system, cs.getContent().toCode(), version);
break; break;
case SUPPLEMENT: case SUPPLEMENT:
ok = rule(errors, "2024-03-06", IssueType.INVALID, stack.getLiteralPath(), false, version == null ? I18nConstants.VALUESET_INCLUDE_CS_SUPPLEMENT : I18nConstants.VALUESET_INCLUDE_CSVER_SUPPLEMENT, system, cs.getSupplements(), version) && ok; ok = rule(errors, "2024-03-06", IssueType.INVALID, stack, false, version == null ? I18nConstants.VALUESET_INCLUDE_CS_SUPPLEMENT : I18nConstants.VALUESET_INCLUDE_CSVER_SUPPLEMENT, system, cs.getSupplements(), version) && ok;
break; break;
default: default:
break; break;
@ -213,7 +281,7 @@ public class ValueSetValidator extends BaseValidator {
} }
if (((InstanceValidator) parent).isValidateValueSetCodesOnTxServer() && batch.size() > 0 & !context.isNoTerminologyServer()) { if (((InstanceValidator) parent).isValidateValueSetCodesOnTxServer() && batch.size() > 0 & !context.isNoTerminologyServer()) {
if (batch.size() > TOO_MANY_CODES_TO_VALIDATE) { if (batch.size() > TOO_MANY_CODES_TO_VALIDATE) {
ok = hint(errors, "2023-09-06", IssueType.BUSINESSRULE, stack.getLiteralPath(), false, I18nConstants.VALUESET_INC_TOO_MANY_CODES, batch.size()) && ok; ok = hint(errors, "2023-09-06", IssueType.BUSINESSRULE, stack, false, I18nConstants.VALUESET_INC_TOO_MANY_CODES, batch.size()) && ok;
} else { } else {
long t = System.currentTimeMillis(); long t = System.currentTimeMillis();
if (parent.isDebug()) { if (parent.isDebug()) {
@ -241,14 +309,12 @@ public class ValueSetValidator extends BaseValidator {
int cf = 0; int cf = 0;
for (Element filter : filters) { for (Element filter : filters) {
if (systemOk && !validateValueSetIncludeFilter(errors, include, stack.push(filter, cf, null, null), system, version, slv)) { ok = validateValueSetIncludeFilter(errors, filter, stack.push(filter, cf, null, null), system, version, cs, slv) & ok;
systemOk = false;
}
cf++; cf++;
} }
slv.finish(include, stack); slv.finish(include, stack);
} else { } else {
warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), filters.size() == 0 && concepts.size() == 0, I18nConstants.VALUESET_NO_SYSTEM_WARNING); warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack, filters.size() == 0 && concepts.size() == 0, I18nConstants.VALUESET_NO_SYSTEM_WARNING);
} }
return ok; return ok;
} }
@ -274,9 +340,9 @@ public class ValueSetValidator extends BaseValidator {
return false; return false;
} else { } else {
boolean ok = vv.isOk(); boolean ok = vv.isOk();
warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), ok, I18nConstants.VALUESET_INCLUDE_INVALID_CONCEPT_CODE, system, code); warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack, ok, I18nConstants.VALUESET_INCLUDE_INVALID_CONCEPT_CODE, system, code);
if (vv.getMessage() != null) { if (vv.getMessage() != null) {
hint(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), false, vv.getMessage()); hint(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack, false, vv.getMessage());
} }
} }
} else { } else {
@ -286,9 +352,9 @@ public class ValueSetValidator extends BaseValidator {
return false; return false;
} else { } else {
boolean ok = vv.isOk(); boolean ok = vv.isOk();
warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), ok, I18nConstants.VALUESET_INCLUDE_INVALID_CONCEPT_CODE_VER, system, version, code); warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack, ok, I18nConstants.VALUESET_INCLUDE_INVALID_CONCEPT_CODE_VER, system, version, code);
if (vv.getMessage() != null) { if (vv.getMessage() != null) {
hint(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), false, vv.getMessage()); hint(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack, false, vv.getMessage());
} }
} }
} }
@ -307,11 +373,274 @@ public class ValueSetValidator extends BaseValidator {
return new VSCodingValidationRequest(stack, c); return new VSCodingValidationRequest(stack, c);
} }
private boolean validateValueSetIncludeFilter(List<ValidationMessage> errors, Element filter, NodeStack push, String system, String version, CodeSystemChecker slv) { private boolean validateValueSetIncludeFilter(List<ValidationMessage> errors, Element filter, NodeStack stack, String system, String version, CodeSystem cs, CodeSystemChecker slv) {
// boolean ok = true;
// String display = concept.getChildValue("display"); String property = filter.getChildValue("property");
// slv.checkConcept(code, display); String op = filter.getChildValue("op");
String value = filter.getChildValue("value");
return true; if (property != null) {
List<String> knownNames = new ArrayList<>();
knownNames.add("concept");
knownNames.add("code");
knownNames.add("status");
knownNames.add("inactive");
knownNames.add("effectiveDate");
knownNames.add("deprecationDate");
knownNames.add("retirementDate");
knownNames.add("notSelectable");
if (cs == null || cs.hasHierarchyMeaning()) {
knownNames.add("parent");
knownNames.add("child");
knownNames.add("partOf");
}
knownNames.add("synonym");
knownNames.add("comment");
knownNames.add("itemWeight");
if (cs != null) {
for (CodeSystemFilterComponent f : cs.getFilter()) {
addName(knownNames, f.getCode());
}
for (PropertyComponent p : cs.getProperty()) {
addName(knownNames, p.getCode());
}
}
for (String s : getSystemKnownNames(system)) {
addName(knownNames, s);
}
boolean pok = false;
if (cs == null) {
pok = hint(errors, "2024-03-09", IssueType.INVALID, stack, knownNames.contains(property), I18nConstants.VALUESET_UNKNOWN_FILTER_PROPERTY_NO_CS, property, system, CommaSeparatedStringBuilder.join(",", knownNames));
} else {
pok = warning(errors, "2024-03-09", IssueType.INVALID, stack, knownNames.contains(property), I18nConstants.VALUESET_UNKNOWN_FILTER_PROPERTY, property, system, CommaSeparatedStringBuilder.join(",", knownNames));
}
if (pok) {
PropertyValidationRules rules = rulesForFilter(system, cs, property);
if (rules != null) {
if (!rules.getOps().isEmpty()) {
ok = rule(errors, "2024-03-09", IssueType.INVALID, stack, opInSet(op, rules.getOps()), I18nConstants.VALUESET_BAD_FILTER_OP, op, property, CommaSeparatedStringBuilder.join(",", rules.getOps())) && ok;
}
if ("exists".equals(op)) {
ok = checkFilterValue(errors, stack, system, version, ok, property, value, PropertyFilterType.Boolean) && ok;
} else if (rules.isRepeating()) {
for (String v : value.split("\\,")) {
ok = checkFilterValue(errors, stack, system, version, ok, property, v, rules.getType()) && ok;
}
} else {
ok = checkFilterValue(errors, stack, system, version, ok, property, value, rules.getType()) && ok;
}
}
}
}
return ok;
}
private boolean opInSet(String op, EnumSet<PropertyOperation> ops) {
switch (op) {
case "=": return ops.contains(PropertyOperation.Equals);
case "is-a": return ops.contains(PropertyOperation.IsA);
case "descendent-of": return ops.contains(PropertyOperation.DescendentOf);
case "is-not-a": return ops.contains(PropertyOperation.IsNotA);
case "regex": return ops.contains(PropertyOperation.RegEx);
case "in": return ops.contains(PropertyOperation.In);
case "not-in": return ops.contains(PropertyOperation.NotIn);
case "generalizes": return ops.contains(PropertyOperation.Generalizes);
case "child-of": return ops.contains(PropertyOperation.ChildOf);
case "descendent-leaf": return ops.contains(PropertyOperation.DescendentLeaf);
case "exists": return ops.contains(PropertyOperation.Exists);
}
return false;
}
private boolean checkFilterValue(List<ValidationMessage> errors, NodeStack stack, String system, String version,boolean ok, String property, String value, PropertyFilterType type) {
if (type != null) {
switch (type) {
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 Code:
ok = rule(errors, "2024-03-09", IssueType.INVALID, stack,
value.trim().equals(value),
I18nConstants.VALUESET_BAD_FILTER_VALUE_CODE, 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))?)?)?)?"),
I18nConstants.VALUESET_BAD_FILTER_VALUE_DATETIME, property, value) && ok;
break;
case Decimal:
ok = rule(errors, "2024-03-09", IssueType.INVALID, stack.getLiteralPath(),
Utilities.isDecimal(value, true),
I18nConstants.VALUESET_BAD_FILTER_VALUE_DECIMAL, property, value) && ok;
break;
case Integer:
ok = rule(errors, "2024-03-09", IssueType.INVALID, stack.getLiteralPath(),
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);
ok = rule(errors, "2024-03-09", IssueType.INVALID, stack, vr.isOk(), I18nConstants.VALUESET_BAD_FILTER_VALUE_CODED_INVALID, property, value, vr.getMessage()) && ok;
}
break;
default:
break;
}
}
return ok;
}
private PropertyValidationRules rulesForFilter(String system, CodeSystem cs, String property) {
var ops = EnumSet.noneOf(PropertyOperation.class);
if (cs != null) {
for (CodeSystemFilterComponent f : cs.getFilter()) {
if (property.equals(f.getCode())) {
for (Enumeration<FilterOperator> op : f.getOperator()) {
ops.add(toOp(op));
}
}
}
for (PropertyComponent p : cs.getProperty()) {
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 STRING: return null;
}
}
}
}
}
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 "comment" : return null;
case "itemWeight" : return new PropertyValidationRules(false, PropertyFilterType.Decimal, 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);
} else if ("CLASSTYPE".equals(property)) {
return new PropertyValidationRules(false, PropertyFilterType.Integer, addToOps(ops, PropertyOperation.Equals, PropertyOperation.In));
} else {
return new PropertyValidationRules(false, PropertyFilterType.ValidCode, 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));
default:
return new PropertyValidationRules(false, PropertyFilterType.ValidCode, 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.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 "code" : return null;
case "telemedicine": return new PropertyValidationRules(false, PropertyFilterType.Boolean, ops);
case "orthopox" : return new PropertyValidationRules(false, PropertyFilterType.Boolean, ops);
}
}
if (ops != null) {
return new PropertyValidationRules(false, null, ops);
} else {
return null;
}
}
private EnumSet<PropertyOperation> addToOps(EnumSet<PropertyOperation> set, PropertyOperation... ops) {
for (PropertyOperation op : ops) {
set.add(op);
}
return set;
}
private PropertyOperation toOp(Enumeration<FilterOperator> op) {
switch (op.getValue()) {
case CHILDOF: return PropertyOperation.ChildOf;
case DESCENDENTLEAF: return PropertyOperation.DescendentLeaf;
case DESCENDENTOF: return PropertyOperation.DescendentOf;
case EQUAL: return PropertyOperation.Equals;
case EXISTS: return PropertyOperation.Exists;
case GENERALIZES: return PropertyOperation.Generalizes;
case IN: return PropertyOperation.In;
case ISA: return PropertyOperation.IsA;
case ISNOTA: return PropertyOperation.IsNotA;
case NOTIN: return PropertyOperation.NotIn;
case REGEX: return PropertyOperation.RegEx;
default: return null;
}
}
private void addName(List<String> knownNames, String code) {
if (code != null && !knownNames.contains(code)) {
knownNames.add(code);
}
}
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://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",
"260686004", "260870009", "263502005", "272741003", "288556008", "363589002", "363698007", "363699004", "363700003", "363701004", "363702006",
"363703001", "363704007", "363705008", "363709002", "363710007", "363713009", "363714003", "370129005", "370130000", "370131001", "370132008",
"370133003", "370134009", "370135005", "371881003", "405813007", "405814001", "405815000", "405816004", "408729009", "408730004", "408731000",
"408732007", "410675002", "411116001", "418775008", "419066007", "424226004", "424244007", "424361007", "424876005", "425391005", "609096000",
"704319004", "704320005", "704321009", "704322002", "704323007", "704324001", "704325000", "704326004", "704327008", "704346009", "704347000",
"704647008", "718497002", "719715003", "719722006", "726542003", "726633004", "732943007", "732945000", "732947008", "733722007", "733725009",
"733928003", "733930001", "733931002", "733932009", "733933004", "734136001", "734137005", "736472000", "736473005", "736474004", "736475003",
"736476002", "736518005", "738774007", "762705008", "762706009", "762949000", "762951001", "763032000", "766939001", "774081006", "774158006",
"774159003", "774160008", "774163005", "827081001", "836358009", "840560000", "860779006", "860781008", "1003703000", "1003735000", "1142135004",
"1142136003", "1142137007", "1142138002", "1142139005", "1142140007", "1142141006", "1142142004", "1142143009", "1148793005", "1148965004",
"1148967007", "1148968002", "1148969005", "1149366004", "1149367008", "1230370004", "320091000221107" };
// list from http://tx.fhir.org/r4/ValueSet/$expand?url=http://snomed.info/sct?fhir_vs=isa/410662002
case "http://www.nlm.nih.gov/research/umls/rxnorm" : return new String[] { "STY", "SAB", "TTY", "SY", "SIB", "RN", "PAR", "CHD", "RB", "RO", "IN", "PIN", "MIN", "BN", "SCD", "SBD", "GPCK", "BPCK", "SCDC", "SCDF", "SCDFP", "SCDG", "SCDGP", "SBDC", "SBDF", "SBDFP", "SBDG", "DF", "DFG" };
case "http://unitsofmeasure.org" : return new String[] { "property", "canonical" };
case "http://www.ama-assn.org/go/cpt" : return new String[] { "modifier", "kind", "modified", "code", "telemedicine", "orthopox" };
case "urn:ietf:bcp:47" : return new String[] {"language", "region", "script", "variant", "extension", "ext-lang", "private-use" };
default: return new String[] { };
}
} }
} }

View File

@ -704,7 +704,6 @@ v: {
"system" : "http://loinc.org", "system" : "http://loinc.org",
"version" : "2.74", "version" : "2.74",
"server" : "http://tx-dev.fhir.org/r4", "server" : "http://tx-dev.fhir.org/r4",
"unknown-systems" : "",
"issues" : { "issues" : {
"resourceType" : "OperationOutcome" "resourceType" : "OperationOutcome"
} }
@ -728,7 +727,6 @@ v: {
"system" : "http://loinc.org", "system" : "http://loinc.org",
"version" : "2.74", "version" : "2.74",
"server" : "http://tx-dev.fhir.org/r4", "server" : "http://tx-dev.fhir.org/r4",
"unknown-systems" : "",
"issues" : { "issues" : {
"resourceType" : "OperationOutcome" "resourceType" : "OperationOutcome"
} }
@ -751,7 +749,6 @@ v: {
"system" : "http://loinc.org", "system" : "http://loinc.org",
"version" : "2.74", "version" : "2.74",
"server" : "http://tx-dev.fhir.org/r4", "server" : "http://tx-dev.fhir.org/r4",
"unknown-systems" : "",
"issues" : { "issues" : {
"resourceType" : "OperationOutcome" "resourceType" : "OperationOutcome"
} }
@ -775,7 +772,6 @@ v: {
"error" : "The provided code 'http://loinc.org#3141-9' was not found in the value set 'http://hl7.org/fhir/ValueSet/observation-vitalsignresult--0|5.0.0'", "error" : "The provided code 'http://loinc.org#3141-9' was not found in the value set 'http://hl7.org/fhir/ValueSet/observation-vitalsignresult--0|5.0.0'",
"class" : "UNKNOWN", "class" : "UNKNOWN",
"server" : "http://tx-dev.fhir.org/r4", "server" : "http://tx-dev.fhir.org/r4",
"unknown-systems" : "",
"issues" : { "issues" : {
"resourceType" : "OperationOutcome", "resourceType" : "OperationOutcome",
"issue" : [{ "issue" : [{
@ -816,7 +812,6 @@ v: {
"system" : "http://loinc.org", "system" : "http://loinc.org",
"version" : "2.74", "version" : "2.74",
"server" : "http://tx-dev.fhir.org/r4", "server" : "http://tx-dev.fhir.org/r4",
"unknown-systems" : "",
"issues" : { "issues" : {
"resourceType" : "OperationOutcome" "resourceType" : "OperationOutcome"
} }
@ -841,7 +836,6 @@ v: {
"error" : "The provided code 'http://loinc.org#3141-9 ('Body weight Measured')' was not found in the value set 'http://hl7.org/fhir/ValueSet/observation-vitalsignresult|5.0.0'", "error" : "The provided code 'http://loinc.org#3141-9 ('Body weight Measured')' was not found in the value set 'http://hl7.org/fhir/ValueSet/observation-vitalsignresult|5.0.0'",
"class" : "UNKNOWN", "class" : "UNKNOWN",
"server" : "http://tx-dev.fhir.org/r4", "server" : "http://tx-dev.fhir.org/r4",
"unknown-systems" : "",
"issues" : { "issues" : {
"resourceType" : "OperationOutcome", "resourceType" : "OperationOutcome",
"issue" : [{ "issue" : [{
@ -882,7 +876,6 @@ v: {
"system" : "http://loinc.org", "system" : "http://loinc.org",
"version" : "2.74", "version" : "2.74",
"server" : "http://tx-dev.fhir.org/r4", "server" : "http://tx-dev.fhir.org/r4",
"unknown-systems" : "",
"issues" : { "issues" : {
"resourceType" : "OperationOutcome" "resourceType" : "OperationOutcome"
} }
@ -906,7 +899,6 @@ v: {
"system" : "http://loinc.org", "system" : "http://loinc.org",
"version" : "2.74", "version" : "2.74",
"server" : "http://tx-dev.fhir.org/r4", "server" : "http://tx-dev.fhir.org/r4",
"unknown-systems" : "",
"issues" : { "issues" : {
"resourceType" : "OperationOutcome" "resourceType" : "OperationOutcome"
} }
@ -930,7 +922,6 @@ v: {
"system" : "http://loinc.org", "system" : "http://loinc.org",
"version" : "2.74", "version" : "2.74",
"server" : "http://tx-dev.fhir.org/r4", "server" : "http://tx-dev.fhir.org/r4",
"unknown-systems" : "",
"issues" : { "issues" : {
"resourceType" : "OperationOutcome" "resourceType" : "OperationOutcome"
} }
@ -955,7 +946,6 @@ v: {
"error" : "Wrong whitespace in Display Name 'Active motion ' for http://loinc.org#LA6715-2. Valid display is 'Active motion' (en-US) (for the language(s) '--')", "error" : "Wrong whitespace in Display Name 'Active motion ' for http://loinc.org#LA6715-2. Valid display is 'Active motion' (en-US) (for the language(s) '--')",
"class" : "UNKNOWN", "class" : "UNKNOWN",
"server" : "http://tx-dev.fhir.org/r4", "server" : "http://tx-dev.fhir.org/r4",
"unknown-systems" : "",
"issues" : { "issues" : {
"resourceType" : "OperationOutcome", "resourceType" : "OperationOutcome",
"issue" : [{ "issue" : [{
@ -997,7 +987,6 @@ v: {
"error" : "Wrong whitespace in Display Name 'Good, strong cry; normal rate and effort of breathing ' for http://loinc.org#LA6727-7. Valid display is 'Good, strong cry; normal rate and effort of breathing' (en-US) (for the language(s) '--')", "error" : "Wrong whitespace in Display Name 'Good, strong cry; normal rate and effort of breathing ' for http://loinc.org#LA6727-7. Valid display is 'Good, strong cry; normal rate and effort of breathing' (en-US) (for the language(s) '--')",
"class" : "UNKNOWN", "class" : "UNKNOWN",
"server" : "http://tx-dev.fhir.org/r4", "server" : "http://tx-dev.fhir.org/r4",
"unknown-systems" : "",
"issues" : { "issues" : {
"resourceType" : "OperationOutcome", "resourceType" : "OperationOutcome",
"issue" : [{ "issue" : [{
@ -1021,3 +1010,43 @@ v: {
} }
------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------
{"code" : {
"system" : "http://loinc.org",
"code" : "LP43571-6"
}, "valueSet" :null, "langs":"", "useServer":"true", "useClient":"true", "guessSystem":"false", "activeOnly":"false", "membershipOnly":"false", "displayWarningMode":"false", "versionFlexible":"true", "profile": {
"resourceType" : "Parameters",
"parameter" : [{
"name" : "profile-url",
"valueString" : "http://hl7.org/fhir/ExpansionProfile/dc8fd4bc-091a-424a-8a3b-6198ef146891"
}]
}}####
v: {
"code" : "LP43571-6",
"severity" : "error",
"error" : "Unknown code 'LP43571-6' in the CodeSystem 'http://loinc.org' version '2.77'",
"class" : "UNKNOWN",
"server" : "http://tx-dev.fhir.org/r4",
"unknown-systems" : "",
"issues" : {
"resourceType" : "OperationOutcome",
"issue" : [{
"extension" : [{
"url" : "http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-server",
"valueUrl" : "http://tx-dev.fhir.org/r4"
}],
"severity" : "error",
"code" : "code-invalid",
"details" : {
"coding" : [{
"system" : "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
"code" : "invalid-code"
}],
"text" : "Unknown code 'LP43571-6' in the CodeSystem 'http://loinc.org' version '2.77'"
},
"location" : ["Coding.code"],
"expression" : ["Coding.code"]
}]
}
}
-------------------------------------------------------------------------------------