complete CQL based Measure Validation

This commit is contained in:
Grahame Grieve 2020-04-07 07:27:59 +10:00
parent 971b59b7af
commit b179c482e7
5 changed files with 139 additions and 3 deletions

View File

@ -470,4 +470,36 @@ public class I18nConstants {
public static final String MEASURE_MR_SCORE_VALUE_INVALID_01 = "MEASURE_MR_SCORE_VALUE_INVALID_01"; public static final String MEASURE_MR_SCORE_VALUE_INVALID_01 = "MEASURE_MR_SCORE_VALUE_INVALID_01";
public static final String MEASURE_MR_SCORE_FIXED = "MEASURE_MR_SCORE_FIXED"; public static final String MEASURE_MR_SCORE_FIXED = "MEASURE_MR_SCORE_FIXED";
public static final String MEASURE_MR_SCORE_UNIT_REQUIRED = "MEASURE_MR_SCORE_UNIT_REQUIRED"; public static final String MEASURE_MR_SCORE_UNIT_REQUIRED = "MEASURE_MR_SCORE_UNIT_REQUIRED";
public static final String MEASURE_M_CRITERIA_UNKNOWN = "MEASURE_M_CRITERIA_UNKNOWN";
public static final String MEASURE_M_CQL_NOT_FOUND = "MEASURE_M_CQL_NOT_FOUND";
public static final String MEASURE_M_CRITERIA_CQL_ERROR = "MEASURE_M_CRITERIA_CQL_ERROR";
public static final String MEASURE_M_CRITERIA_CQL_ONLY_ONE_LIB = "MEASURE_M_CRITERIA_CQL_ONLY_ONE_LIB";
public static final String MEASURE_M_CRITERIA_CQL_NO_LIB = "MEASURE_M_CRITERIA_CQL_NO_LIB";
public static final String MEASURE_M_CRITERIA_CQL_LIB_NOT_FOUND = "MEASURE_M_CRITERIA_CQL_LIB_NOT_FOUND";
public static final String MEASURE_M_CRITERIA_CQL_LIB_DUPL = "MEASURE_M_CRITERIA_CQL_LIB_DUPL";
public static final String MEASURE_M_CRITERIA_CQL_NO_ELM = "MEASURE_M_CRITERIA_CQL_NO_ELM";
public static final String MEASURE_M_CRITERIA_CQL_ELM_NOT_VALID = "MEASURE_M_CRITERIA_CQL_ELM_NOT_VALID";
public static final String MEASURE_M_CRITERIA_CQL_NOT_FOUND = "MEASURE_M_CRITERIA_CQL_NOT_FOUND";
//public static final String
//public static final String
//public static final String
//public static final String
//public static final String
//public static final String
//public static final String
//public static final String
//public static final String
//public static final String
//public static final String
//public static final String
//public static final String
//public static final String
//public static final String
//public static final String
//public static final String
//public static final String
//public static final String
//public static final String
//public static final String
//public static final String
} }

View File

@ -458,6 +458,20 @@ public class XMLUtil {
return builder.parse(new ByteArrayInputStream(content.getBytes())); return builder.parse(new ByteArrayInputStream(content.getBytes()));
} }
public static Document parseToDom(byte[] content) throws ParserConfigurationException, SAXException, IOException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(false);
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(new ByteArrayInputStream(content));
}
public static Document parseToDom(byte[] content, boolean ns) throws ParserConfigurationException, SAXException, IOException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(ns);
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(new ByteArrayInputStream(content));
}
public static Document parseFileToDom(String filename) throws ParserConfigurationException, SAXException, IOException { public static Document parseFileToDom(String filename) throws ParserConfigurationException, SAXException, IOException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(false); factory.setNamespaceAware(false);

View File

@ -470,3 +470,14 @@ MEASURE_MR_SCORE_VALUE_REQUIRED = A value is required when the Measure.scoring={
MEASURE_MR_SCORE_VALUE_INVALID_01 = The value is invalid - it must be between 0 and 1 MEASURE_MR_SCORE_VALUE_INVALID_01 = The value is invalid - it must be between 0 and 1
MEASURE_MR_SCORE_FIXED = This value is fixed by the Measure to ''{0]'' MEASURE_MR_SCORE_FIXED = This value is fixed by the Measure to ''{0]''
MEASURE_MR_SCORE_UNIT_REQUIRED = A unit should be present when the scoring type is {0} MEASURE_MR_SCORE_UNIT_REQUIRED = A unit should be present when the scoring type is {0}
MEASURE_M_CRITERIA_UNKNOWN = The expression language {0} is not supported, so can''t be validated
MEASURE_M_CQL_NOT_FOUND = None of the include CQL Libraries define a function {0}
MEASURE_M_CRITERIA_CQL_NO_LIB = No CQL Libraries found on this Measure
MEASURE_M_CRITERIA_CQL_ONLY_ONE_LIB = If the CQL expression does not include a namespace, there can only be one Library for the measure
MEASURE_M_CRITERIA_CQL_LIB_NOT_FOUND = No matching Library found for the namespace {0}
MEASURE_M_CRITERIA_CQL_LIB_DUPL = Multiple matching libraies found for the namespace {0}
MEASURE_M_CRITERIA_CQL_ERROR = Error in {0}: ''{1}''
MEASURE_M_CRITERIA_CQL_NO_ELM = Error in {0}: No compiled version of CQL found
MEASURE_M_CRITERIA_CQL_ELM_NOT_VALID = = Error in {0}: Compiled version of CQL is not valid
MEASURE_M_CRITERIA_CQL_NOT_FOUND = The function {1} does not exist in the library {0}

View File

@ -1,17 +1,24 @@
package org.hl7.fhir.validation.instance.type; package org.hl7.fhir.validation.instance.type;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.xml.parsers.ParserConfigurationException;
import org.hl7.fhir.r5.elementmodel.Element; import org.hl7.fhir.r5.elementmodel.Element;
import org.hl7.fhir.r5.model.Attachment;
import org.hl7.fhir.r5.model.Coding; import org.hl7.fhir.r5.model.Coding;
import org.hl7.fhir.r5.model.Library; import org.hl7.fhir.r5.model.Library;
import org.hl7.fhir.r5.model.Measure; import org.hl7.fhir.r5.model.Measure;
import org.hl7.fhir.r5.model.Measure.MeasureGroupComponent; import org.hl7.fhir.r5.model.Measure.MeasureGroupComponent;
import org.hl7.fhir.utilities.xml.XMLUtil;
import org.xml.sax.SAXException;
public class MeasureContext { public class MeasureContext {
public static final String USER_DATA_ELM = "validator.ELM";
private List<Library> libs = new ArrayList<>(); private List<Library> libs = new ArrayList<>();
private Measure measure; private Measure measure;
private Element report; private Element report;
@ -26,6 +33,15 @@ public class MeasureContext {
public void seeLibrary(Library l) { public void seeLibrary(Library l) {
libs.add(l); libs.add(l);
for (Attachment att : l.getContent()) {
if ("application/elm+xml".equals(att.getContentType())) {
try {
l.setUserData(USER_DATA_ELM, XMLUtil.parseToDom(att.getData(), true));
} catch (Exception e) {
l.setUserData(USER_DATA_ELM, e.getMessage());
}
}
}
} }
public List<MeasureGroupComponent> groups() { public List<MeasureGroupComponent> groups() {
@ -42,5 +58,8 @@ public class MeasureContext {
public String scoring() { public String scoring() {
return measure.getScoring().getCodingFirstRep().getCode(); return measure.getScoring().getCodingFirstRep().getCode();
} }
public List<Library> libraries() {
return libs;
}
} }

View File

@ -32,10 +32,12 @@ import org.hl7.fhir.utilities.i18n.I18nConstants;
import org.hl7.fhir.utilities.validation.ValidationMessage; 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.validation.ValidationMessage.Source; import org.hl7.fhir.utilities.validation.ValidationMessage.Source;
import org.hl7.fhir.utilities.xml.XMLUtil;
import org.hl7.fhir.validation.instance.utils.NodeStack; import org.hl7.fhir.validation.instance.utils.NodeStack;
import org.hl7.fhir.validation.BaseValidator; import org.hl7.fhir.validation.BaseValidator;
import org.hl7.fhir.validation.TimeTracker; import org.hl7.fhir.validation.TimeTracker;
import org.hl7.fhir.validation.instance.utils.ValidatorHostContext; import org.hl7.fhir.validation.instance.utils.ValidatorHostContext;
import org.w3c.dom.Document;
import net.sf.saxon.tree.tiny.LargeStringBuffer; import net.sf.saxon.tree.tiny.LargeStringBuffer;
@ -104,10 +106,68 @@ public class MeasureValidator extends BaseValidator {
} }
private void validateMeasureCriteria(ValidatorHostContext hostContext, List<ValidationMessage> errors, MeasureContext mctxt, Element crit, NodeStack nsc) { private void validateMeasureCriteria(ValidatorHostContext hostContext, List<ValidationMessage> errors, MeasureContext mctxt, Element crit, NodeStack nsc) {
// TODO Auto-generated method stub String mimeType = crit.getChildValue("language");
if (!Utilities.noString(mimeType)) { // that would be an error elsewhere
if ("text/cql".equals(mimeType)) {
String cqlRef = crit.getChildValue("expression");
Library lib = null;
if (rule(errors, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), mctxt.libraries().size()> 0, I18nConstants.MEASURE_M_CRITERIA_CQL_NO_LIB)) {
if (cqlRef.contains(".")) {
String name = cqlRef.substring(0, cqlRef.indexOf("."));
cqlRef = cqlRef.substring(cqlRef.indexOf(".")+1);
for (Library l : mctxt.libraries()) {
if (l.getName().equals(name)) {
if (rule(errors, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), lib == null, I18nConstants.MEASURE_M_CRITERIA_CQL_LIB_DUPL)) {
lib = l;
}
}
}
rule(errors, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), lib != null, I18nConstants.MEASURE_M_CRITERIA_CQL_LIB_NOT_FOUND, name);
} else {
if (rule(errors, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), mctxt.libraries().size() == 1, I18nConstants.MEASURE_M_CRITERIA_CQL_ONLY_ONE_LIB)) {
lib = mctxt.libraries().get(0);
}
}
}
if (lib != null) {
if (rule(errors, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), lib.hasUserData(MeasureContext.USER_DATA_ELM), I18nConstants.MEASURE_M_CRITERIA_CQL_NO_ELM, lib.getUrl())) {
if (lib.getUserData(MeasureContext.USER_DATA_ELM) instanceof String) {
rule(errors, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), false, I18nConstants.MEASURE_M_CRITERIA_CQL_ERROR, lib.getUrl(), lib.getUserString(MeasureContext.USER_DATA_ELM));
} else if (lib.getUserData(MeasureContext.USER_DATA_ELM) instanceof Document) {
org.w3c.dom.Element elm = ((Document)lib.getUserData(MeasureContext.USER_DATA_ELM)).getDocumentElement();
if (rule(errors, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), isValidElm(elm), I18nConstants.MEASURE_M_CRITERIA_CQL_ELM_NOT_VALID, lib.getUrl(), cqlRef)) {
rule(errors, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), hasCqlTarget(elm, cqlRef), I18nConstants.MEASURE_M_CRITERIA_CQL_NOT_FOUND, lib.getUrl(), cqlRef);
}
}
}
}
} else if ("text/fhirpath".equals(mimeType)) {
warning(errors, IssueType.REQUIRED, crit.line(), crit.col(), nsc.getLiteralPath(), false, I18nConstants.MEASURE_M_CRITERIA_UNKNOWN, mimeType);
} else if ("application/x-fhir-query".equals(mimeType)) {
warning(errors, IssueType.REQUIRED, crit.line(), crit.col(), nsc.getLiteralPath(), false, I18nConstants.MEASURE_M_CRITERIA_UNKNOWN, mimeType);
} else {
warning(errors, IssueType.REQUIRED, crit.line(), crit.col(), nsc.getLiteralPath(), false, I18nConstants.MEASURE_M_CRITERIA_UNKNOWN, mimeType);
}
}
} }
private boolean isValidElm(org.w3c.dom.Element elm) {
return elm != null && "library".equals(elm.getNodeName()) && "urn:hl7-org:elm:r1".equals(elm.getNamespaceURI());
}
private boolean hasCqlTarget(org.w3c.dom.Element element, String cqlRef) {
org.w3c.dom.Element stmts = XMLUtil.getNamedChild(element, "statements");
if (stmts != null) {
for (org.w3c.dom.Element def : XMLUtil.getNamedChildren(stmts, "def")) {
if (cqlRef.equals(def.getAttribute("name"))) {
return true;
}
}
}
return false;
}
// --------------------------------------------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------------------------------------------
public void validateMeasureReport(ValidatorHostContext hostContext, List<ValidationMessage> errors, Element element, NodeStack stack) throws FHIRException { public void validateMeasureReport(ValidatorHostContext hostContext, List<ValidationMessage> errors, Element element, NodeStack stack) throws FHIRException {