Improve MeasureReport validation for checking subject count

This commit is contained in:
Grahame Grieve 2024-07-16 06:16:47 +08:00
parent 37006946cd
commit 512f70aa18
5 changed files with 177 additions and 32 deletions

View File

@ -1093,4 +1093,9 @@ public class I18nConstants {
public static final String IG_DEPENDENCY_EXCEPTION = "IG_DEPENDENCY_EXCEPTION"; public static final String IG_DEPENDENCY_EXCEPTION = "IG_DEPENDENCY_EXCEPTION";
public static final String IG_DEPENDENCY_PACKAGE_UNKNOWN = "IG_DEPENDENCY_PACKAGE_UNKNOWN"; public static final String IG_DEPENDENCY_PACKAGE_UNKNOWN = "IG_DEPENDENCY_PACKAGE_UNKNOWN";
public static final String NDJSON_EMPTY_LINE_WARNING = "NDJSON_EMPTY_LINE_WARNING"; public static final String NDJSON_EMPTY_LINE_WARNING = "NDJSON_EMPTY_LINE_WARNING";
public static final String MEASURE_MR_GRP_POP_COUNT_CANT_CHECK = "MEASURE_MR_GRP_POP_COUNT_CANT_CHECK";
public static final String MEASURE_MR_GRP_POP_COUNT_NO_REF = "MEASURE_MR_GRP_POP_COUNT_NO_REF";
public static final String MEASURE_MR_GRP_POP_COUNT_UNRESOLVED = "MEASURE_MR_GRP_POP_COUNT_UNRESOLVED";
public static final String MEASURE_MR_GRP_POP_COUNT_NO_REF_RES = "MEASURE_MR_GRP_POP_COUNT_NO_REF_RES";
public static final String MEASURE_MR_GRP_POP_COUNT_REF_UNPROCESSIBLE = "MEASURE_MR_GRP_POP_COUNT_REF_UNPROCESSIBLE";
} }

View File

@ -371,6 +371,11 @@ MEASURE_MR_GRP_NO_CODE = Group should have a code that matches the group definit
MEASURE_MR_GRP_NO_USABLE_CODE = None of the codes provided are usable for comparison - need both system and code on at least one code MEASURE_MR_GRP_NO_USABLE_CODE = None of the codes provided are usable for comparison - need both system and code on at least one code
MEASURE_MR_GRP_NO_WRONG_CODE = The code provided ({0}) does not match the code specified in the measure report ({1}) MEASURE_MR_GRP_NO_WRONG_CODE = The code provided ({0}) does not match the code specified in the measure report ({1})
MEASURE_MR_GRP_POP_COUNT_MISMATCH = Mismatch between count {0} and number of subjects {1} MEASURE_MR_GRP_POP_COUNT_MISMATCH = Mismatch between count {0} and number of subjects {1}
MEASURE_MR_GRP_POP_COUNT_CANT_CHECK = Unable to check the stated count {0} because the subject list cannot be fully processed ({1})
MEASURE_MR_GRP_POP_COUNT_NO_REF = Subject reference has no actual reference
MEASURE_MR_GRP_POP_COUNT_UNRESOLVED = Subject reference {0} could not be resolved, and the apparent type is {1} which could not be processed
MEASURE_MR_GRP_POP_COUNT_NO_REF_RES = Subject reference {0} could not be resolved, and so could not be processed
MEASURE_MR_GRP_POP_COUNT_REF_UNPROCESSIBLE = Subject reference {0} resolved to a {1}, which could not be processed
MEASURE_MR_GRP_POP_DUPL_CODE = The code for this group population is duplicated with another group MEASURE_MR_GRP_POP_DUPL_CODE = The code for this group population is duplicated with another group
MEASURE_MR_GRP_POP_NO_CODE = Group should have a code that matches the group population definition in the measure MEASURE_MR_GRP_POP_NO_CODE = Group should have a code that matches the group population definition in the measure
MEASURE_MR_GRP_POP_NO_COUNT = Count should be present for reports where type is not ''subject-list'' MEASURE_MR_GRP_POP_NO_COUNT = Count should be present for reports where type is not ''subject-list''

View File

@ -4564,7 +4564,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
return true; return true;
} }
private ResolvedReference localResolve(String ref, NodeStack stack, List<ValidationMessage> errors, String path, Element rootResource, Element groupingResource, Element source, BooleanHolder bh) { public ResolvedReference localResolve(String ref, NodeStack stack, List<ValidationMessage> errors, String path, Element rootResource, Element groupingResource, Element source, BooleanHolder bh) {
if (ref.startsWith("#")) { if (ref.startsWith("#")) {
// work back through the parent list, tracking the stack as we go // work back through the parent list, tracking the stack as we go
// really, there should only be one level for this (contained resources cannot contain // really, there should only be one level for this (contained resources cannot contain

View File

@ -25,6 +25,7 @@ import org.hl7.fhir.r5.model.Measure.MeasureGroupStratifierComponent;
import org.hl7.fhir.r5.model.Resource; import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.renderers.DataRenderer; import org.hl7.fhir.r5.renderers.DataRenderer;
import org.hl7.fhir.r5.utils.ToolingExtensions; import org.hl7.fhir.r5.utils.ToolingExtensions;
import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
import org.hl7.fhir.utilities.FhirPublication; import org.hl7.fhir.utilities.FhirPublication;
import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.i18n.I18nConstants; import org.hl7.fhir.utilities.i18n.I18nConstants;
@ -32,7 +33,10 @@ import org.hl7.fhir.utilities.validation.ValidationMessage;
import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
import org.hl7.fhir.utilities.xml.XMLUtil; import org.hl7.fhir.utilities.xml.XMLUtil;
import org.hl7.fhir.validation.BaseValidator; import org.hl7.fhir.validation.BaseValidator;
import org.hl7.fhir.validation.BaseValidator.BooleanHolder;
import org.hl7.fhir.validation.instance.InstanceValidator;
import org.hl7.fhir.validation.instance.utils.NodeStack; import org.hl7.fhir.validation.instance.utils.NodeStack;
import org.hl7.fhir.validation.instance.utils.ResolvedReference;
import org.hl7.fhir.validation.instance.utils.ValidationContext; import org.hl7.fhir.validation.instance.utils.ValidationContext;
import org.w3c.dom.Document; import org.w3c.dom.Document;
@ -493,21 +497,95 @@ public class MeasureValidator extends BaseValidator {
private boolean validateMeasureReportGroupPopulation(ValidationContext hostContext, MeasureContext m, MeasureGroupPopulationComponent mgp, List<ValidationMessage> errors, Element mrgp, NodeStack ns, boolean inProgress) { private boolean validateMeasureReportGroupPopulation(ValidationContext hostContext, MeasureContext m, MeasureGroupPopulationComponent mgp, List<ValidationMessage> errors, Element mrgp, NodeStack ns, boolean inProgress) {
boolean ok = true; boolean ok = true;
List<Element> sr = mrgp.getChildrenByName("subjectResults"); List<Element> srl = mrgp.getChildrenByName("subjectResults");
if ("subject-list".equals(m.reportType())) { if ("subject-list".equals(m.reportType())) {
try { try {
int subCount = 0;
CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder("; ");
for (Element sr : srl) {
subCount = addToSubjectCount(subCount, hostContext, sr, m, ns, b);
}
if (mrgp.hasChild("count")) {
int c = Integer.parseInt(mrgp.getChildValue("count")); int c = Integer.parseInt(mrgp.getChildValue("count"));
ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrgp.line(), mrgp.col(), ns.getLiteralPath(), c == sr.size(), I18nConstants.MEASURE_MR_GRP_POP_COUNT_MISMATCH, c, sr.size()) && ok; if (subCount > -1) {
ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrgp.line(), mrgp.col(), ns.getLiteralPath(), c == subCount, I18nConstants.MEASURE_MR_GRP_POP_COUNT_MISMATCH, c, subCount) && ok;
} else {
hint(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrgp.line(), mrgp.col(), ns.getLiteralPath(), false, I18nConstants.MEASURE_MR_GRP_POP_COUNT_CANT_CHECK, c, b.toString());
}
}
} catch (Exception e) { } catch (Exception e) {
// nothing; that'll be because count is not valid, and that's a different error or its missing and we don't care // nothing; that'll be because count is not valid, and that's a different error or its missing and we don't care
} }
} else { } else {
ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrgp.line(), mrgp.col(), ns.getLiteralPath(), sr.size() == 0, I18nConstants.MEASURE_MR_GRP_POP_NO_SUBJECTS) && ok; ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrgp.line(), mrgp.col(), ns.getLiteralPath(), srl.size() == 0, I18nConstants.MEASURE_MR_GRP_POP_NO_SUBJECTS) && ok;
warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrgp.line(), mrgp.col(), ns.getLiteralPath(), mrgp.hasChild("count", false), I18nConstants.MEASURE_MR_GRP_POP_NO_COUNT); warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrgp.line(), mrgp.col(), ns.getLiteralPath(), mrgp.hasChild("count", false), I18nConstants.MEASURE_MR_GRP_POP_NO_COUNT);
} }
return ok; return ok;
} }
private int addToSubjectCount(int subCount, ValidationContext valContext, Element sr, MeasureContext m, NodeStack ns, CommaSeparatedStringBuilder b) throws FHIRException, IOException {
if (subCount < 0) {
return -1;
}
String ref = sr.getNamedChildValue("reference");
if (ref == null) {
b.append(context.formatMessage(I18nConstants.MEASURE_MR_GRP_POP_COUNT_NO_REF));
return -1;
}
BooleanHolder bh = new BooleanHolder();
ResolvedReference rr = ((InstanceValidator) parent).localResolve(ref, ns, new ArrayList<>(), ns.getLiteralPath(), valContext.getRootResource(), valContext.getGroupingResource(), sr, bh);
Element tgt;
if (rr != null) {
tgt = rr.getResource();
} else {
tgt = fetcher.fetch(((InstanceValidator) parent), valContext.getAppContext(), ref);
}
if (tgt == null) {
// we couldn't resolve it, but we'll draw our own conclusion from the literal URL if we can.
String[] parts = ref.split("\\/");
if (parts.length == 2 && context.getResourceNamesAsSet().contains(parts[0])) {
switch (parts[0]) {
case "Patient":
case "Practitioner":
case "Person":
case "PractitionerRole":
case "RelatedPerson":
return subCount + 1;
case "List":
b.append(context.formatMessage(I18nConstants.MEASURE_MR_GRP_POP_COUNT_UNRESOLVED, "List", ref));
return -1; // for now
case "Group":
b.append(context.formatMessage(I18nConstants.MEASURE_MR_GRP_POP_COUNT_UNRESOLVED, "Group", ref));
return -1; // for now
default:
b.append(context.formatMessage(I18nConstants.MEASURE_MR_GRP_POP_COUNT_UNRESOLVED, parts[0], ref));
return -1;
}
} else {
// add information / hint?
b.append(context.formatMessage(I18nConstants.MEASURE_MR_GRP_POP_COUNT_NO_REF_RES, ref));
return -1;
}
}
switch (tgt.fhirType()) {
case "Patient":
case "Practitioner":
case "Person":
case "PractitionerRole":
case "RelatedPerson":
return subCount + 1;
case "List":
return subCount + tgt.getChildren("entry").size();
case "Group":
b.append(context.formatMessage(I18nConstants.MEASURE_MR_GRP_POP_COUNT_REF_UNPROCESSIBLE, "Group", ref));
return -1; // for now
default:
b.append(context.formatMessage(I18nConstants.MEASURE_MR_GRP_POP_COUNT_REF_UNPROCESSIBLE, tgt.fhirType(), ref));
return -1;
}
}
private boolean validateMeasureReportGroupStratifiers(ValidationContext hostContext, MeasureContext m, MeasureGroupComponent mg, List<ValidationMessage> errors, Element mrg, NodeStack stack, boolean inProgress) { private boolean validateMeasureReportGroupStratifiers(ValidationContext hostContext, MeasureContext m, MeasureGroupComponent mg, List<ValidationMessage> errors, Element mrg, NodeStack stack, boolean inProgress) {
boolean ok = true; boolean ok = true;

View File

@ -3587,10 +3587,67 @@ v: {
"code" : "en-AU", "code" : "en-AU",
"system" : "urn:ietf:bcp:47", "system" : "urn:ietf:bcp:47",
"server" : "http://tx-dev.fhir.org/r4", "server" : "http://tx-dev.fhir.org/r4",
"unknown-systems" : "",
"issues" : { "issues" : {
"resourceType" : "OperationOutcome" "resourceType" : "OperationOutcome"
} }
} }
------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------
{"code" : {
"system" : "urn:oid:1.2.840.114350.1.72.1.7.7.10.696784.13260",
"code" : "1",
"display" : "Surgery Case"
}, "url": "http://terminology.hl7.org/ValueSet/v3-ActEncounterCode", "version": "3.0.0", "langs":"", "useServer":"true", "useClient":"true", "guessSystem":"false", "activeOnly":"false", "membershipOnly":"false", "displayWarningMode":"false", "versionFlexible":"false", "profile": {
"resourceType" : "Parameters",
"parameter" : [{
"name" : "profile-url",
"valueString" : "http://hl7.org/fhir/ExpansionProfile/dc8fd4bc-091a-424a-8a3b-6198ef146891"
}]
}}####
v: {
"code" : "1",
"severity" : "error",
"error" : "A definition for CodeSystem 'urn:oid:1.2.840.114350.1.72.1.7.7.10.696784.13260' could not be found, so the code cannot be validated; The provided code 'urn:oid:1.2.840.114350.1.72.1.7.7.10.696784.13260#1 ('Surgery Case')' was not found in the value set 'http://terminology.hl7.org/ValueSet/v3-ActEncounterCode|3.0.0'",
"class" : "UNKNOWN",
"server" : "http://tx-dev.fhir.org/r4",
"unknown-systems" : "urn:oid:1.2.840.114350.1.72.1.7.7.10.696784.13260",
"issues" : {
"resourceType" : "OperationOutcome",
"issue" : [{
"extension" : [{
"url" : "http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-server",
"valueUrl" : "http://tx-dev.fhir.org/r4"
}],
"severity" : "error",
"code" : "not-found",
"details" : {
"coding" : [{
"system" : "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
"code" : "not-found"
}],
"text" : "A definition for CodeSystem 'urn:oid:1.2.840.114350.1.72.1.7.7.10.696784.13260' could not be found, so the code cannot be validated"
},
"location" : ["Coding.system"],
"expression" : ["Coding.system"]
},
{
"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" : "not-in-vs"
}],
"text" : "The provided code 'urn:oid:1.2.840.114350.1.72.1.7.7.10.696784.13260#1 ('Surgery Case')' was not found in the value set 'http://terminology.hl7.org/ValueSet/v3-ActEncounterCode|3.0.0'"
},
"location" : ["Coding.code"],
"expression" : ["Coding.code"]
}]
}
}
-------------------------------------------------------------------------------------