2024 07 gg measure validation (#1687)

* Fix for validator using wrong property for list determination when parsing json

* Fix for R2B Resource.id cardinality problem

* Improve MeasureReport validation for checking subject count

* work around THO 6.0.0 problem (hack, to be reversed later)

* release notes

---------

Co-authored-by: Grahame Grieve <grahameg@gmail.ccom>
This commit is contained in:
Grahame Grieve 2024-07-16 23:43:36 +08:00 committed by GitHub
parent a09c712c87
commit 9272b6eff3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 204 additions and 37 deletions

View File

@ -1,7 +1,12 @@
## Validator Changes ## Validator Changes
* no changes * work around THO 6.0.0 problem (hack, to be reversed later)
* Improve MeasureReport validation for checking subject count
* Fix for R2B Resource.id cardinality problem
* Fix for validator using wrong property for list determination when parsing json
## Other code changes ## Other code changes
* no changes * no changes

View File

@ -564,6 +564,7 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
public Map<String, NamingSystem> getNSUrlMap() { public Map<String, NamingSystem> getNSUrlMap() {
if (systemUrlMap == null) { if (systemUrlMap == null) {
systemUrlMap = new HashMap<>(); systemUrlMap = new HashMap<>();
try {
List<NamingSystem> nsl = systems.getList(); List<NamingSystem> nsl = systems.getList();
for (NamingSystem ns : nsl) { for (NamingSystem ns : nsl) {
for (NamingSystemUniqueIdComponent uid : ns.getUniqueId()) { for (NamingSystemUniqueIdComponent uid : ns.getUniqueId()) {
@ -572,6 +573,12 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
} }
} }
} }
} catch (Exception e) {
if (!nsFailHasFailed) {
e.printStackTrace();
nsFailHasFailed = true;
}
}
} }
return systemUrlMap; return systemUrlMap;
} }
@ -2530,6 +2537,7 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
protected IWorkerContextManager.IPackageLoadingTracker packageTracker; protected IWorkerContextManager.IPackageLoadingTracker packageTracker;
private boolean forPublication; private boolean forPublication;
private boolean cachingAllowed = true; private boolean cachingAllowed = true;
private static boolean nsFailHasFailed;
public Resource fetchResourceById(String type, String uri, FhirPublication fhirVersion) { public Resource fetchResourceById(String type, String uri, FhirPublication fhirVersion) {
return fetchResourceById(type, uri); return fetchResourceById(type, uri);

View File

@ -280,7 +280,7 @@ public class Property {
} }
public boolean isList() { public boolean isList() {
return !"1".equals(definition.getMax()); return !"1".equals(definition.getBase().hasMax() ? definition.getBase().getMax() : definition.getMax());
} }
public boolean isBaseList() { public boolean isBaseList() {

View File

@ -828,7 +828,6 @@ public class DataRenderer extends Renderer implements CodeResolver {
x.tx(context.formatPhrase(RenderingContext.DATA_REND_BASE64, length)); x.tx(context.formatPhrase(RenderingContext.DATA_REND_BASE64, length));
} else { } else {
x.code(type.primitiveValue()); x.code(type.primitiveValue());
} }
break; break;
default: default:

View File

@ -78,6 +78,16 @@ public class PackageHackerR5 {
} }
} }
} }
// work around an r2b issue
if (packageInfo.getId().equals("hl7.fhir.r2b.core") && r.getType().equals("StructureDefinition")) {
StructureDefinition sd = (StructureDefinition) r.getResource();
for (ElementDefinition ed : sd.getSnapshot().getElement()) {
if (ed.getPath().equals(sd.getType()+".id")) {
ed.getBase().setMax("1");
}
}
}
// work around a r4 version of extension pack issue // work around a r4 version of extension pack issue
if (packageInfo.getId().equals("hl7.fhir.uv.extensions.r4") && r.getType().equals("StructureDefinition")) { if (packageInfo.getId().equals("hl7.fhir.uv.extensions.r4") && r.getType().equals("StructureDefinition")) {
StructureDefinition sd = (StructureDefinition) r.getResource(); StructureDefinition sd = (StructureDefinition) r.getResource();

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"]
}]
}
}
-------------------------------------------------------------------------------------

View File

@ -21,7 +21,7 @@
<commons_compress_version>1.26.0</commons_compress_version> <commons_compress_version>1.26.0</commons_compress_version>
<guava_version>32.0.1-jre</guava_version> <guava_version>32.0.1-jre</guava_version>
<hapi_fhir_version>6.4.1</hapi_fhir_version> <hapi_fhir_version>6.4.1</hapi_fhir_version>
<validator_test_case_version>1.5.15</validator_test_case_version> <validator_test_case_version>1.5.16-SNAPSHOT</validator_test_case_version>
<jackson_version>2.17.0</jackson_version> <jackson_version>2.17.0</jackson_version>
<junit_jupiter_version>5.9.2</junit_jupiter_version> <junit_jupiter_version>5.9.2</junit_jupiter_version>
<junit_platform_launcher_version>1.8.2</junit_platform_launcher_version> <junit_platform_launcher_version>1.8.2</junit_platform_launcher_version>