diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java index e451911ce..b4eed7307 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java @@ -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_FIXED = "MEASURE_MR_SCORE_FIXED"; 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 } \ No newline at end of file diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xml/XMLUtil.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xml/XMLUtil.java index e204a5d01..41b9f6cbc 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xml/XMLUtil.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xml/XMLUtil.java @@ -458,6 +458,20 @@ public class XMLUtil { 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 { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(false); diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index 586b75858..2213ea1f4 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -469,4 +469,15 @@ MEASURE_MR_SCORE_UNIT_PROHIBITED = A measureScore for this Measure Scoring ({0}) MEASURE_MR_SCORE_VALUE_REQUIRED = A value is required when the Measure.scoring={0} 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_UNIT_REQUIRED = A unit should be present when the scoring type is {0} \ No newline at end of file +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} + diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/MeasureContext.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/MeasureContext.java index 903933bb5..a6d411f84 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/MeasureContext.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/MeasureContext.java @@ -1,17 +1,24 @@ package org.hl7.fhir.validation.instance.type; +import java.io.IOException; import java.util.ArrayList; import java.util.List; +import javax.xml.parsers.ParserConfigurationException; + 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.Library; import org.hl7.fhir.r5.model.Measure; import org.hl7.fhir.r5.model.Measure.MeasureGroupComponent; +import org.hl7.fhir.utilities.xml.XMLUtil; +import org.xml.sax.SAXException; public class MeasureContext { + public static final String USER_DATA_ELM = "validator.ELM"; private List libs = new ArrayList<>(); private Measure measure; private Element report; @@ -26,6 +33,15 @@ public class MeasureContext { public void seeLibrary(Library 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 groups() { @@ -42,5 +58,8 @@ public class MeasureContext { public String scoring() { return measure.getScoring().getCodingFirstRep().getCode(); } + public List libraries() { + return libs; + } } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/MeasureValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/MeasureValidator.java index 2e3dc153f..a2f2c978a 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/MeasureValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/MeasureValidator.java @@ -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.IssueType; 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.BaseValidator; import org.hl7.fhir.validation.TimeTracker; import org.hl7.fhir.validation.instance.utils.ValidatorHostContext; +import org.w3c.dom.Document; import net.sf.saxon.tree.tiny.LargeStringBuffer; @@ -104,12 +106,70 @@ public class MeasureValidator extends BaseValidator { } private void validateMeasureCriteria(ValidatorHostContext hostContext, List 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 errors, Element element, NodeStack stack) throws FHIRException { Element m = element.getNamedChild("measure"); String measure = null;