Start building support for measure validation

This commit is contained in:
Grahame Grieve 2020-03-31 10:14:12 +11:00
parent 092a694a3f
commit 232f1ff8ff
5 changed files with 168 additions and 6 deletions

View File

@ -57,6 +57,7 @@ import org.hl7.fhir.r5.model.ElementDefinition;
import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionBindingComponent;
import org.hl7.fhir.r5.model.Enumerations.PublicationStatus;
import org.hl7.fhir.r5.model.ImplementationGuide;
import org.hl7.fhir.r5.model.Measure;
import org.hl7.fhir.r5.model.CanonicalResource;
import org.hl7.fhir.r5.model.NamingSystem;
import org.hl7.fhir.r5.model.NamingSystem.NamingSystemIdentifierType;
@ -142,6 +143,7 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
private CanonicalResourceManager<ConceptMap> maps = new CanonicalResourceManager<ConceptMap>(false);
protected CanonicalResourceManager<StructureMap> transforms = new CanonicalResourceManager<StructureMap>(false);
private CanonicalResourceManager<StructureDefinition> structures = new CanonicalResourceManager<StructureDefinition>(false);
private CanonicalResourceManager<Measure> measures = new CanonicalResourceManager<Measure>(false);
private CanonicalResourceManager<ImplementationGuide> guides = new CanonicalResourceManager<ImplementationGuide>(false);
private CanonicalResourceManager<CapabilityStatement> capstmts = new CanonicalResourceManager<CapabilityStatement>(false);
private CanonicalResourceManager<SearchParameter> searchParameters = new CanonicalResourceManager<SearchParameter>(false);
@ -207,6 +209,7 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
systems.addAll(other.systems);
guides.copy(other.guides);
capstmts.copy(other.capstmts);
measures.copy(other.measures);
allowLoadingDuplicates = other.allowLoadingDuplicates;
tsServer = other.tsServer;
@ -257,6 +260,8 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
guides.see((ImplementationGuide) m);
else if (r instanceof CapabilityStatement)
capstmts.see((CapabilityStatement) m);
else if (r instanceof Measure)
measures.see((Measure) m);
else if (r instanceof SearchParameter)
searchParameters.see((SearchParameter) m);
else if (r instanceof PlanDefinition)
@ -817,6 +822,8 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
return (T) guides.get(uri);
if (capstmts.has(uri))
return (T) capstmts.get(uri);
if (measures.has(uri))
return (T) measures.get(uri);
if (valueSets.has(uri))
return (T) valueSets.get(uri);
if (codeSystems.has(uri))
@ -847,6 +854,8 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
return (T) guides.get(uri);
} else if (class_ == CapabilityStatement.class) {
return (T) capstmts.get(uri);
} else if (class_ == Measure.class) {
return (T) measures.get(uri);
} else if (class_ == StructureDefinition.class) {
return (T) structures.get(uri);
} else if (class_ == StructureMap.class) {
@ -1024,6 +1033,7 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
json.addProperty("structures-count", structures.size());
json.addProperty("guides-count", guides.size());
json.addProperty("statements-count", capstmts.size());
json.addProperty("measures-count", measures.size());
}
}
@ -1049,6 +1059,8 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
guides.drop(id);
else if (fhirType.equals("CapabilityStatement"))
capstmts.drop(id);
else if (fhirType.equals("Measure"))
measures.drop(id);
else if (fhirType.equals("ValueSet"))
valueSets.drop(id);
else if (fhirType.equals("CodeSystem"))
@ -1088,6 +1100,7 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
structures.listAllM(result);
guides.listAllM(result);
capstmts.listAllM(result);
measures.listAllM(result);
codeSystems.listAllM(result);
valueSets.listAllM(result);
maps.listAllM(result);
@ -1269,6 +1282,9 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
if (capstmts.has(url))
return capstmts.get(url).getUserString("path");
if (measures.has(url))
return measures.get(url).getUserString("path");
if (searchParameters.has(url))
return searchParameters.get(url).getUserString("path");

View File

@ -59,14 +59,17 @@ MustSupport_VAL_MustSupport = The element {0} is not marked as ''mustSupport'' i
Profile_EXT_Not_Here = The extension {0} is not allowed to be used at this point (based on context invariant "{1}")
Profile_VAL_MissingElement = Missing element "{0}" - required by fixed value assigned in profile {1}
Profile_VAL_NotAllowed = The element {0} is present in the instance but not allowed in the applicable {1} specified in profile
Measure_MR_M_None = No Measure is identified, so no validation can be performed against the Measure
Measure_MR_M_NotFound = The Measure "{0}" could not be resolved, so no validation can be performed against the Measure
Questionnaire_QR_Item_BadOption = The value provided ({0}::{1}) is not in the options value set in the questionnaire
Questionnaire_QR_Item_Coding = Error {0} validating Coding against Questionnaire Options
Questionnaire_QR_Item_CodingNoOptions = Cannot validate Coding option because no option list is provided
Questionnaire_QR_Item_DateNoOptions = Cannot validate date answer option because no option list is provided
Questionnaire_QR_Item_Display = Items not of type DISPLAY should not have items - linkId {0}
Questionnaire_QR_Item_Group = Items of type group should not have answers
Questionnaire_QR_Item_GroupAnswer = Items not of type group should not have items outside answers (use answer.item not .item)
Questionnaire_QR_Item_IntNoOptions = Cannot validate integer answer option because no option list is provided
Questionnaire_QR_Item_Missing = No response answer found for required item {0}
Questionnaire_QR_Item_Missing = No response answer found for required item "{0}"
Questionnaire_QR_Item_NoCoding = The code {0}::{1} is not a valid option
Questionnaire_QR_Item_NoDate = The date {0} is not a valid option
Questionnaire_QR_Item_NoInteger = The integer {0} is not a valid option
@ -235,6 +238,8 @@ Unable_to_find_element_with_id_ = Unable to find element with id "{0}"
Slice_encountered_midway_through_set_path___id___ = Slice encountered midway through set (path = {0}, id = {1}); {2}
Unable_to_resolve_actual_type_ = Unable to resolve actual type {0}
Unsupported_version_R1 = Unsupported version R1
Unsupported_version_R2 = Unsupported version R2
Unsupported_version_R2B = Unsupported version R2B
Unsupported_fixed_value_type_for_discriminator_for_slice__ = Unsupported fixed value type for discriminator({0}) for slice {1}: {2}
Unsupported_CodeableConcept_pattern__extensions_are_not_allowed__for_discriminator_for_slice_ = Unsupported CodeableConcept pattern - extensions are not allowed - for discriminator({0}) for slice {1}
Unsupported_CodeableConcept_pattern__must_have_at_least_one_coding__for_discriminator_for_slice_ = Unsupported CodeableConcept pattern - must have at least one coding - for discriminator({0}) for slice {1}

View File

@ -26,6 +26,7 @@ import java.util.stream.*;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r5.elementmodel.Element;
import org.hl7.fhir.r5.model.*;
import org.hl7.fhir.r5.model.MeasureReport.MeasureReportGroupComponent;
import org.hl7.fhir.r5.model.Questionnaire.*;
import org.hl7.fhir.r5.utils.FHIRPathEngine;
import org.hl7.fhir.validation.instance.utils.ValidatorHostContext;
@ -93,6 +94,58 @@ public class EnableWhenEvaluator {
}
}
public static class MeasurePair {
private MeasureReportGroupComponent g;
private Element a;
public MeasurePair(MeasureReportGroupComponent g, Element a) {
super();
this.g = g;
this.a = a;
}
public MeasureReportGroupComponent getGroup() {
return g;
}
public Element getA() {
return a;
}
}
public static class MStack extends ArrayList<MeasurePair> {
private static final long serialVersionUID = 1L;
private Measure m;
private Element a;
public MStack(Measure m, Element a) {
super();
this.m = m;
this.a = a;
}
public Measure getM() {
return m;
}
public Element getA() {
return a;
}
public MStack push(MeasureReportGroupComponent g, Element a) {
MStack self = new MStack(this.m, this.a);
self.addAll(this);
self.add(new MeasurePair(g, a));
return self;
}
}
public static class EnableWhenResult {
private final boolean enabled;
private final QuestionnaireItemEnableWhenComponent enableWhenCondition;

View File

@ -95,6 +95,7 @@ import org.hl7.fhir.r5.model.HumanName;
import org.hl7.fhir.r5.model.Identifier;
import org.hl7.fhir.r5.model.InstantType;
import org.hl7.fhir.r5.model.IntegerType;
import org.hl7.fhir.r5.model.Measure;
import org.hl7.fhir.r5.model.Period;
import org.hl7.fhir.r5.model.Quantity;
import org.hl7.fhir.r5.model.Questionnaire;
@ -130,6 +131,7 @@ import org.hl7.fhir.r5.utils.ToolingExtensions;
import org.hl7.fhir.utilities.i18n.I18nConstants;
import org.hl7.fhir.validation.BaseValidator;
import org.hl7.fhir.validation.instance.EnableWhenEvaluator.QStack;
import org.hl7.fhir.validation.instance.EnableWhenEvaluator.MStack;
import org.hl7.fhir.validation.XVerExtensionManager;
import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
import org.hl7.fhir.utilities.Utilities;
@ -3491,6 +3493,8 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
validateQuestionannaireItem(errors, element, element, stack, parents);
} else if (element.getType().equals("QuestionnaireResponse")) {
validateQuestionannaireResponse(hostContext, errors, element, stack);
} else if (element.getType().equals("MeasureReport")) {
validateMeasureReport(hostContext, errors, element, stack);
} else if (element.getType().equals("CapabilityStatement")) {
validateCapabilityStatement(errors, element, stack);
} else if (element.getType().equals("CodeSystem")) {
@ -3717,6 +3721,77 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
}
}
private void validateMeasureReport(ValidatorHostContext hostContext, List<ValidationMessage> errors, Element element, NodeStack stack) throws FHIRException {
Element m = element.getNamedChild("measure");
String measure = null;
if (m != null) {
/*
* q.getValue() is correct for R4 content, but we'll also accept the second
* option just in case we're validating raw STU3 content. Being lenient here
* isn't the end of the world since if someone is actually doing the reference
* wrong in R4 content it'll get flagged elsewhere by the validator too
*/
if (isNotBlank(m.getValue())) {
measure = m.getValue();
} else if (isNotBlank(m.getChildValue("reference"))) {
measure = m.getChildValue("reference");
}
}
if (hint(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), measure != null, I18nConstants.MEASURE_MR_M_NONE)) {
long t = System.nanoTime();
Measure msrc = measure.startsWith("#") ? loadMeasure(element, measure.substring(1)) : context.fetchResource(Measure.class, measure);
sdTime = sdTime + (System.nanoTime() - t);
if (warning(errors, IssueType.REQUIRED, m.line(), m.col(), stack.getLiteralPath(), msrc != null, I18nConstants.MEASURE_MR_M_NOTFOUND, measure)) {
boolean inComplete = !"complete".equals(element.getNamedChildValue("status"));
//validateMeasureReportGroup(hostContext, msrc, msrc.getGroup(), errors, element, stack, inComplete, element, new MStack(msrc, element));
}
}
}
private Measure loadMeasure(Element resource, String id) throws FHIRException {
try {
for (Element contained : resource.getChildren("contained")) {
if (contained.getIdBase().equals(id)) {
FhirPublication v = FhirPublication.fromCode(context.getVersion());
ByteArrayOutputStream bs = new ByteArrayOutputStream();
new JsonParser(context).compose(contained, bs, OutputStyle.NORMAL, id);
byte[] json = bs.toByteArray();
switch (v) {
case DSTU1:
throw new FHIRException(context.formatMessage(I18nConstants.UNSUPPORTED_VERSION_R1));
case DSTU2:
throw new FHIRException(context.formatMessage(I18nConstants.UNSUPPORTED_VERSION_R2));
case DSTU2016May:
throw new FHIRException(context.formatMessage(I18nConstants.UNSUPPORTED_VERSION_R2B));
case STU3:
org.hl7.fhir.dstu3.model.Resource r3 = new org.hl7.fhir.dstu3.formats.JsonParser().parse(json);
Resource r5 = VersionConvertor_30_50.convertResource(r3, false);
if (r5 instanceof Measure)
return (Measure) r5;
else
return null;
case R4:
org.hl7.fhir.r4.model.Resource r4 = new org.hl7.fhir.r4.formats.JsonParser().parse(json);
r5 = VersionConvertor_40_50.convertResource(r4);
if (r5 instanceof Measure)
return (Measure) r5;
else
return null;
case R5:
r5 = new org.hl7.fhir.r5.formats.JsonParser().parse(json);
if (r5 instanceof Measure)
return (Measure) r5;
else
return null;
}
}
}
return null;
} catch (IOException e) {
throw new FHIRException(e);
}
}
private Questionnaire loadQuestionnaire(Element resource, String id) throws FHIRException {
try {
for (Element contained : resource.getChildren("contained")) {
@ -3782,7 +3857,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
else if (myEnableWhenEvaluator.isQuestionEnabled(hostContext, qItem, qstack, fpe)) {
rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), isAnswerRequirementFulfilled(qItem, answers), I18nConstants.QUESTIONNAIRE_QR_ITEM_MISSING, qItem.getLinkId());
} else if (!answers.isEmpty()) { // items without answers should be allowed, but not items with answers to questions that are disabled
// it appears that this is always a duplicate error - it will always already have beeb reported, so no need to report it again?
// it appears that this is always a duplicate error - it will always already have been reported, so no need to report it again?
// GDG 2019-07-13
// rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), !isAnswerRequirementFulfilled(qItem, answers), I18nConstants.QUESTIONNAIRE_QR_ITEM_NOTENABLED, qItem.getLinkId());
}
@ -3866,7 +3941,10 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
break;
}
}
validateQuestionannaireResponseItems(hostContext, qsrc, qItem.getItem(), errors, answer, stack, inProgress, questionnaireResponseRoot, qstack);
if (qItem.getType() != QuestionnaireItemType.GROUP) {
// if it's a group, we already have an error before getting here, so no need to hammer away on that
validateQuestionannaireResponseItems(hostContext, qsrc, qItem.getItem(), errors, answer, stack, inProgress, questionnaireResponseRoot, qstack);
}
}
if (qItem.getType() == null) {
fail(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOTYPE, qItem.getLinkId());
@ -3874,8 +3952,12 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
List<Element> items = new ArrayList<Element>();
element.getNamedChildren("item", items);
rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), items.isEmpty(), I18nConstants.QUESTIONNAIRE_QR_ITEM_DISPLAY, qItem.getLinkId());
} else if (qItem.getType() != QuestionnaireItemType.GROUP) {
List<Element> items = new ArrayList<Element>();
element.getNamedChildren("item", items);
rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), items.isEmpty(), I18nConstants.QUESTIONNAIRE_QR_ITEM_GROUP_ANSWER, qItem.getLinkId());
} else {
validateQuestionannaireResponseItems(hostContext, qsrc, qItem.getItem(), errors, element, stack, inProgress, questionnaireResponseRoot, qstack);
validateQuestionannaireResponseItems(hostContext, qsrc, qItem.getItem(), errors, element, stack, inProgress, questionnaireResponseRoot, qstack);
}
}
@ -3960,7 +4042,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
// item is missing, is the question enabled?
if (enabled && qItem.getRequired()) {
String message = "No response found for required item with id = '" + qItem.getLinkId() + "'";
String message = context.formatMessage(I18nConstants.QUESTIONNAIRE_QR_ITEM_MISSING, qItem.getLinkId());
if (inProgress) {
warning(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, message);
} else {
@ -4510,7 +4592,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
}
if (ref != null && !Utilities.noString(reference)) {
if (ref != null && !Utilities.noString(reference) && !reference.startsWith("#")) {
Element target = resolveInBundle(entries, reference, fullUrl, type, id);
rule(errors, IssueType.INVALID, ref.line(), ref.col(), stack.addToLiteralPath("reference"), target != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_NOTFOUND, reference, name);
}
@ -4620,6 +4702,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
composition.getNamedChildren(propName, list);
int i = 1;
for (Element elem : list) {
validateBundleReference(errors, entries, elem, title + "." + propName, stack.push(elem, i, null, null), fullUrl, "Composition", id);
i++;
}

View File

@ -146,6 +146,11 @@ public class ValidationTestSuite implements IEvaluationContext, IValidatorResour
String contents = TestingUtilities.loadTestResource("validator", filename);
vCurr.getContext().cacheResource(loadResource(filename, contents));
}
if (content.has("measure")) {
String filename = content.get("measure").getAsString();
String contents = TestingUtilities.loadTestResource("validator", filename);
vCurr.getContext().cacheResource(loadResource(filename, contents));
}
if (content.has("codesystems")) {
for (JsonElement je : content.getAsJsonArray("codesystems")) {
String filename = je.getAsString();