001package org.hl7.fhir.validation.instance.type; 002 003import static org.apache.commons.lang3.StringUtils.isNotBlank; 004 005import java.io.ByteArrayOutputStream; 006import java.io.IOException; 007import java.math.BigDecimal; 008import java.util.ArrayList; 009import java.util.List; 010 011import org.hl7.fhir.convertors.factory.VersionConvertorFactory_30_50; 012import org.hl7.fhir.convertors.factory.VersionConvertorFactory_40_50; 013import org.hl7.fhir.exceptions.FHIRException; 014import org.hl7.fhir.r5.context.IWorkerContext; 015import org.hl7.fhir.r5.elementmodel.Element; 016import org.hl7.fhir.r5.elementmodel.JsonParser; 017import org.hl7.fhir.r5.elementmodel.ObjectConverter; 018import org.hl7.fhir.r5.formats.IParser.OutputStyle; 019import org.hl7.fhir.r5.model.CodeableConcept; 020import org.hl7.fhir.r5.model.Coding; 021import org.hl7.fhir.r5.model.Library; 022import org.hl7.fhir.r5.model.Measure; 023import org.hl7.fhir.r5.model.Measure.MeasureGroupComponent; 024import org.hl7.fhir.r5.model.Measure.MeasureGroupPopulationComponent; 025import org.hl7.fhir.r5.model.Measure.MeasureGroupStratifierComponent; 026import org.hl7.fhir.r5.model.Resource; 027import org.hl7.fhir.r5.renderers.DataRenderer; 028import org.hl7.fhir.r5.utils.ToolingExtensions; 029import org.hl7.fhir.r5.utils.XVerExtensionManager; 030import org.hl7.fhir.utilities.FhirPublication; 031import org.hl7.fhir.utilities.Utilities; 032import org.hl7.fhir.utilities.i18n.I18nConstants; 033import org.hl7.fhir.utilities.validation.ValidationMessage; 034import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; 035import org.hl7.fhir.utilities.validation.ValidationMessage.Source; 036import org.hl7.fhir.utilities.xml.XMLUtil; 037import org.hl7.fhir.validation.BaseValidator; 038import org.hl7.fhir.validation.TimeTracker; 039import org.hl7.fhir.validation.instance.InstanceValidator; 040import org.hl7.fhir.validation.instance.utils.NodeStack; 041import org.hl7.fhir.validation.instance.utils.ValidatorHostContext; 042import org.w3c.dom.Document; 043 044public class MeasureValidator extends BaseValidator { 045 046 private InstanceValidator parent; 047 public MeasureValidator(IWorkerContext context, TimeTracker timeTracker, XVerExtensionManager xverManager, Coding jurisdiction, InstanceValidator parent) { 048 super(context, xverManager); 049 source = Source.InstanceValidator; 050 this.timeTracker = timeTracker; 051 this.jurisdiction = jurisdiction; 052 this.parent = parent; 053 054 } 055 056 public boolean validateMeasure(ValidatorHostContext hostContext, List<ValidationMessage> errors, Element element, NodeStack stack) throws FHIRException { 057 boolean ok = true; 058 MeasureContext mctxt = new MeasureContext(); 059 List<Element> libs = element.getChildrenByName("library"); 060 for (Element lib : libs) { 061 String ref = lib.isPrimitive() ? lib.primitiveValue() : lib.getChildValue("reference"); 062 if (!Utilities.noString(ref)) { 063 Library l = context.fetchResource(Library.class, ref); 064 if (hint(errors, NO_RULE_DATE, IssueType.NOTFOUND, lib.line(), lib.col(), stack.getLiteralPath(), l != null, I18nConstants.MEASURE_M_LIB_UNKNOWN, ref)) { 065 mctxt.seeLibrary(l); 066 } 067 } 068 } 069 070 List<Element> groups = element.getChildrenByName("group"); 071 if (warning(errors, NO_RULE_DATE, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), groups.size() > 0, I18nConstants.MEASURE_M_NO_GROUPS)) { 072 int c = 0; 073 for (Element group : groups) { 074 NodeStack ns = stack.push(group, c, null, null); 075 warning(errors, NO_RULE_DATE, IssueType.REQUIRED, group.line(), group.col(), ns.getLiteralPath(), groups.size() ==1 || group.hasChild("code"), I18nConstants.MEASURE_M_GROUP_CODE); 076 warning(errors, NO_RULE_DATE, IssueType.REQUIRED, group.line(), group.col(), ns.getLiteralPath(), group.hasChildren("population"), I18nConstants.MEASURE_M_GROUP_POP); 077 int c1 = 0; 078 List<Element> pl = group.getChildrenByName("population"); 079 for (Element p : pl) { 080 NodeStack ns2 = ns.push(p, c1, null, null); 081 warning(errors, NO_RULE_DATE, IssueType.REQUIRED, p.line(), p.col(), ns2.getLiteralPath(), pl.size() == 1 || p.hasChild("code"), I18nConstants.MEASURE_M_GROUP_POP_NO_CODE); 082 c1++; 083 } 084 c1 = 0; 085 List<Element> stl = group.getChildrenByName("stratifier"); 086 for (Element st : stl) { 087 NodeStack ns2 = ns.push(st, c1, null, null); 088 warning(errors, NO_RULE_DATE, IssueType.REQUIRED, st.line(), st.col(), ns2.getLiteralPath(), stl.size() == 1 || st.hasChild("code"), I18nConstants.MEASURE_M_GROUP_STRATA_NO_CODE); 089 if (st.hasChild("criteria")) { 090 Element crit = st.getNamedChild("criteria"); 091 NodeStack nsc = ns2.push(crit, -1, null, null); 092 ok = validateMeasureCriteria(hostContext, errors, mctxt, crit, nsc) && ok; 093 } 094 int c2 = 0; 095 List<Element> cpl = group.getChildrenByName("component"); 096 for (Element cp : cpl) { 097 NodeStack ns3 = ns2.push(cp, c2, null, null); 098 warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cp.line(), cp.col(), ns3.getLiteralPath(), cpl.size() == 1 || cp.hasChild("code"), I18nConstants.MEASURE_M_GROUP_STRATA_COMP_NO_CODE); 099 if (cp.hasChild("criteria")) { 100 Element crit = cp.getNamedChild("criteria"); 101 NodeStack nsc = ns3.push(crit, -1, null, null); 102 ok= validateMeasureCriteria(hostContext, errors, mctxt, crit, nsc) && ok; 103 } 104 c2++; 105 } 106 c1++; 107 } 108 c++; 109 } 110 } 111 if (!stack.isContained()) { 112 ok = checkShareableMeasure(errors, element, stack) && ok; 113 } 114 return ok; 115 } 116 117 118 private boolean checkShareableMeasure(List<ValidationMessage> errors, Element cs, NodeStack stack) { 119 boolean ok = true; 120 if (parent.isForPublication()) { 121 if (isHL7(cs)) { 122 ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("url"), I18nConstants.MEASURE_SHAREABLE_MISSING_HL7, "url") && ok; 123 ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("version"), I18nConstants.MEASURE_SHAREABLE_MISSING_HL7, "version") && ok; 124 ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("name"), I18nConstants.MEASURE_SHAREABLE_EXTRA_MISSING_HL7, "name") && ok; 125 warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("title"), I18nConstants.MEASURE_SHAREABLE_MISSING_HL7, "title"); 126 ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("status"), I18nConstants.MEASURE_SHAREABLE_MISSING_HL7, "status") && ok; 127 ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("experimental"), I18nConstants.MEASURE_SHAREABLE_MISSING_HL7, "experimental") && ok; 128 ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("publisher"), I18nConstants.MEASURE_SHAREABLE_MISSING_HL7, "publisher") && ok; 129 ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("description"), I18nConstants.MEASURE_SHAREABLE_MISSING_HL7, "description") && ok; 130 } else { 131 warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("url"), I18nConstants.MEASURE_SHAREABLE_MISSING, "url"); 132 warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("version"), I18nConstants.MEASURE_SHAREABLE_MISSING, "version"); 133 warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("name"), I18nConstants.MEASURE_SHAREABLE_EXTRA_MISSING, "name"); 134 warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("title"), I18nConstants.MEASURE_SHAREABLE_MISSING, "title"); 135 warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("status"), I18nConstants.MEASURE_SHAREABLE_MISSING, "status"); 136 warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("experimental"), I18nConstants.MEASURE_SHAREABLE_MISSING, "experimental"); 137 warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("description"), I18nConstants.MEASURE_SHAREABLE_MISSING, "description"); 138 warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("publisher"), I18nConstants.MEASURE_SHAREABLE_MISSING, "publisher"); 139 } 140 } 141 return ok; 142 } 143 144 private boolean validateMeasureCriteria(ValidatorHostContext hostContext, List<ValidationMessage> errors, MeasureContext mctxt, Element crit, NodeStack nsc) { 145 boolean ok = true; 146 String mimeType = crit.getChildValue("language"); 147 if (!Utilities.noString(mimeType)) { // that would be an error elsewhere 148 if ("text/cql".equals(mimeType) || "text/cql.identifier".equals(mimeType)) { 149 String cqlRef = crit.getChildValue("expression"); 150 Library lib = null; 151 if (rule(errors, NO_RULE_DATE, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), mctxt.libraries().size()> 0, I18nConstants.MEASURE_M_CRITERIA_CQL_NO_LIB)) { 152 if (cqlRef.contains(".")) { 153 String name = cqlRef.substring(0, cqlRef.indexOf(".")); 154 cqlRef = cqlRef.substring(cqlRef.indexOf(".")+1); 155 for (Library l : mctxt.libraries()) { 156 if (name.equals(l.getName())) { 157 if (rule(errors, NO_RULE_DATE, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), lib == null, I18nConstants.MEASURE_M_CRITERIA_CQL_LIB_DUPL)) { 158 lib = l; 159 } else { 160 ok = false; 161 } 162 } 163 } 164 ok = rule(errors, NO_RULE_DATE, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), lib != null, I18nConstants.MEASURE_M_CRITERIA_CQL_LIB_NOT_FOUND, name) && ok; 165 } else { 166 if (rule(errors, NO_RULE_DATE, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), mctxt.libraries().size() == 1, I18nConstants.MEASURE_M_CRITERIA_CQL_ONLY_ONE_LIB)) { 167 lib = mctxt.libraries().get(0); 168 } else { 169 ok = false; 170 } 171 } 172 } else { 173 ok = false; 174 } 175 if (lib != null) { 176 if (rule(errors, NO_RULE_DATE, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), lib.hasUserData(MeasureContext.USER_DATA_ELM), I18nConstants.MEASURE_M_CRITERIA_CQL_NO_ELM, lib.getUrl())) { 177 if (lib.getUserData(MeasureContext.USER_DATA_ELM) instanceof String) { 178 ok = rule(errors, NO_RULE_DATE, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), false, I18nConstants.MEASURE_M_CRITERIA_CQL_ERROR, lib.getUrl(), lib.getUserString(MeasureContext.USER_DATA_ELM)) && ok; 179 } else if (lib.getUserData(MeasureContext.USER_DATA_ELM) instanceof Document) { 180 org.w3c.dom.Element elm = ((Document)lib.getUserData(MeasureContext.USER_DATA_ELM)).getDocumentElement(); 181 if (rule(errors, NO_RULE_DATE, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), isValidElm(elm), I18nConstants.MEASURE_M_CRITERIA_CQL_ELM_NOT_VALID, lib.getUrl(), cqlRef)) { 182 ok = rule(errors, NO_RULE_DATE, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), hasCqlTarget(elm, cqlRef), I18nConstants.MEASURE_M_CRITERIA_CQL_NOT_FOUND, lib.getUrl(), cqlRef) && ok; 183 } 184 } 185 } else { 186 ok = false; 187 } 188 } 189 } else if ("text/fhirpath".equals(mimeType)) { 190 warning(errors, NO_RULE_DATE, IssueType.REQUIRED, crit.line(), crit.col(), nsc.getLiteralPath(), false, I18nConstants.MEASURE_M_CRITERIA_UNKNOWN, mimeType); 191 } else if ("application/x-fhir-query".equals(mimeType)) { 192 warning(errors, NO_RULE_DATE, IssueType.REQUIRED, crit.line(), crit.col(), nsc.getLiteralPath(), false, I18nConstants.MEASURE_M_CRITERIA_UNKNOWN, mimeType); 193 } else { 194 warning(errors, NO_RULE_DATE, IssueType.REQUIRED, crit.line(), crit.col(), nsc.getLiteralPath(), false, I18nConstants.MEASURE_M_CRITERIA_UNKNOWN, mimeType); 195 } 196 } 197 return ok; 198 } 199 200 private boolean isValidElm(org.w3c.dom.Element elm) { 201 return elm != null && "library".equals(elm.getNodeName()) && "urn:hl7-org:elm:r1".equals(elm.getNamespaceURI()); 202 } 203 204 private boolean hasCqlTarget(org.w3c.dom.Element element, String cqlRef) { 205 org.w3c.dom.Element stmts = XMLUtil.getNamedChild(element, "statements"); 206 if (stmts != null) { 207 for (org.w3c.dom.Element def : XMLUtil.getNamedChildren(stmts, "def")) { 208 if (cqlRef.equals(def.getAttribute("name"))) { 209 return true; 210 } 211 } 212 } 213 return false; 214 } 215 216 217 // --------------------------------------------------------------------------------------------------------------------------------------------------------- 218 219 public boolean validateMeasureReport(ValidatorHostContext hostContext, List<ValidationMessage> errors, Element element, NodeStack stack) throws FHIRException { 220 boolean ok = true; 221 Element m = element.getNamedChild("measure"); 222 String measure = null; 223 if (m != null) { 224 /* 225 * q.getValue() is correct for R4 content, but we'll also accept the second 226 * option just in case we're validating raw STU3 content. Being lenient here 227 * isn't the end of the world since if someone is actually doing the reference 228 * wrong in R4 content it'll get flagged elsewhere by the validator too 229 */ 230 if (isNotBlank(m.getValue())) { 231 measure = m.getValue(); 232 } else if (isNotBlank(m.getChildValue("reference"))) { 233 measure = m.getChildValue("reference"); 234 } 235 } 236 if (hint(errors, NO_RULE_DATE, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), measure != null, I18nConstants.MEASURE_MR_M_NONE)) { 237 long t = System.nanoTime(); 238 Measure msrc = measure.startsWith("#") ? loadMeasure(element, measure.substring(1)) : context.fetchResource(Measure.class, measure); 239 timeTracker.sd(t); 240 if (warning(errors, NO_RULE_DATE, IssueType.REQUIRED, m.line(), m.col(), stack.getLiteralPath(), msrc != null, I18nConstants.MEASURE_MR_M_NOTFOUND, measure)) { 241 boolean inComplete = !"complete".equals(element.getNamedChildValue("status")); 242 MeasureContext mc = new MeasureContext(msrc, element); 243 NodeStack ns = stack.push(m, -1, m.getProperty().getDefinition(), m.getProperty().getDefinition()); 244 hint(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, m.line(), m.col(), ns.getLiteralPath(), Utilities.existsInList(mc.scoring(), "proportion", "ratio", "continuous-variable", "cohort"), I18nConstants.MEASURE_MR_M_SCORING_UNK); 245 ok = validateMeasureReportGroups(hostContext, mc, errors, element, stack, inComplete) && ok; 246 } 247 } 248 return ok; 249 } 250 251 private Measure loadMeasure(Element resource, String id) throws FHIRException { 252 try { 253 for (Element contained : resource.getChildren("contained")) { 254 if (contained.getIdBase().equals(id)) { 255 FhirPublication v = FhirPublication.fromCode(context.getVersion()); 256 ByteArrayOutputStream bs = new ByteArrayOutputStream(); 257 new JsonParser(context).compose(contained, bs, OutputStyle.NORMAL, id); 258 byte[] json = bs.toByteArray(); 259 switch (v) { 260 case DSTU1: 261 throw new FHIRException(context.formatMessage(I18nConstants.UNSUPPORTED_VERSION_R1)); 262 case DSTU2: 263 throw new FHIRException(context.formatMessage(I18nConstants.UNSUPPORTED_VERSION_R2)); 264 case DSTU2016May: 265 throw new FHIRException(context.formatMessage(I18nConstants.UNSUPPORTED_VERSION_R2B)); 266 case STU3: 267 org.hl7.fhir.dstu3.model.Resource r3 = new org.hl7.fhir.dstu3.formats.JsonParser().parse(json); 268 Resource r5 = VersionConvertorFactory_30_50.convertResource(r3); 269 if (r5 instanceof Measure) 270 return (Measure) r5; 271 else 272 return null; 273 case R4: 274 org.hl7.fhir.r4.model.Resource r4 = new org.hl7.fhir.r4.formats.JsonParser().parse(json); 275 r5 = VersionConvertorFactory_40_50.convertResource(r4); 276 if (r5 instanceof Measure) 277 return (Measure) r5; 278 else 279 return null; 280 case R5: 281 r5 = new org.hl7.fhir.r5.formats.JsonParser().parse(json); 282 if (r5 instanceof Measure) 283 return (Measure) r5; 284 else 285 return null; 286 } 287 } 288 } 289 return null; 290 } catch (IOException e) { 291 throw new FHIRException(e); 292 } 293 } 294 295 private boolean validateMeasureReportGroups(ValidatorHostContext hostContext, MeasureContext m, List<ValidationMessage> errors, Element mr, NodeStack stack, boolean inProgress) { 296 boolean ok = true; 297 298 if (m.groups().size() == 0) { 299 // only validate the report groups if the measure has groups. 300 return ok; 301 } 302 303 List<MeasureGroupComponent> groups = new ArrayList<MeasureGroupComponent>(); 304 305 List<Element> glist = mr.getChildrenByName("group"); 306 307 if (glist.size() == 1 && m.groups().size() == 1) { 308 // if there's only one group, it can be ((and usually is) anonymous) 309 // but we still check that the code, if both have one, is consistent. 310 Element mrg = glist.get(0); 311 NodeStack ns = stack.push(mrg, 0, mrg.getProperty().getDefinition(), mrg.getProperty().getDefinition()); 312 if (m.groups().get(0).hasCode() && mrg.hasChild("code")) { 313 CodeableConcept cc = ObjectConverter.readAsCodeableConcept(mrg.getNamedChild("code")); 314 if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), hasUseableCode(cc), I18nConstants.MEASURE_MR_GRP_NO_USABLE_CODE)) { 315 ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), cc.matches(m.groups().get(0).getCode()), I18nConstants.MEASURE_MR_GRP_NO_WRONG_CODE, DataRenderer.display(context, cc), DataRenderer.display(context, m.groups().get(0).getCode())) && ok; 316 } else { 317 ok = false; 318 } 319 } 320 ok = validateMeasureReportGroup(hostContext, m, m.groups().get(0), errors, mrg, ns, inProgress) && ok; 321 } else { 322 int i = 0; 323 for (Element mrg : glist) { 324 NodeStack ns = stack.push(mrg, i, mrg.getProperty().getDefinition(), mrg.getProperty().getDefinition()); 325 CodeableConcept cc = ObjectConverter.readAsCodeableConcept(mrg.getNamedChild("code")); 326 if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), cc != null, I18nConstants.MEASURE_MR_GRP_NO_CODE)) { 327 MeasureGroupComponent mg = getGroupForCode(cc, m.measure()); 328 if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), mg != null, I18nConstants.MEASURE_MR_GRP_UNK_CODE)) { 329 if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), !groups.contains(mg), I18nConstants.MEASURE_MR_GRP_DUPL_CODE)) { 330 groups.add(mg); 331 ok = validateMeasureReportGroup(hostContext, m, mg, errors, mrg, ns, inProgress) && ok; 332 } else { 333 ok = false; 334 } 335 } else { 336 ok = false; 337 } 338 } else { 339 ok = false; 340 } 341 i++; 342 } 343 boolean dataCollection = isDataCollection(mr); 344 for (MeasureGroupComponent mg : m.groups()) { 345 if (!groups.contains(mg)) { 346 ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mr.line(), mr.col(), stack.getLiteralPath(), groups.contains(mg) || dataCollection, I18nConstants.MEASURE_MR_GRP_MISSING_BY_CODE, DataRenderer.display(context, mg.getCode())) && ok; 347 } 348 } 349 } 350 return ok; 351 } 352 353 private boolean isDataCollection(Element mr) { 354 return "data-collection".equals(mr.getChildValue("type")); 355 } 356 357 private boolean validateMeasureReportGroup(ValidatorHostContext hostContext, MeasureContext m, MeasureGroupComponent mg, List<ValidationMessage> errors, Element mrg, NodeStack ns, boolean inProgress) { 358 boolean ok = true; 359 ok = validateMeasureReportGroupPopulations(hostContext, m, mg, errors, mrg, ns, inProgress) && ok; 360 ok = validateScore(hostContext, m, errors, mrg, ns, inProgress) && ok; 361 ok = validateMeasureReportGroupStratifiers(hostContext, m, mg, errors, mrg, ns, inProgress) && ok; 362 return ok; 363 } 364 365 private boolean validateScore(ValidatorHostContext hostContext, MeasureContext m, List<ValidationMessage> errors, Element mrg, NodeStack stack, boolean inProgress) { 366 boolean ok = true; 367 368 Element ms = mrg.getNamedChild("measureScore"); 369 // first, we check MeasureReport.type 370 if ("data-collection".equals(m.reportType())) { 371 ok = banned(errors, stack, ms, I18nConstants.MEASURE_MR_SCORE_PROHIBITED_RT) && ok; 372 } else if ("cohort".equals(m.scoring())) { 373 // cohort - there is no measure score 374 ok = banned(errors, stack, ms, I18nConstants.MEASURE_MR_SCORE_PROHIBITED_MS) && ok; 375 } else if (Utilities.existsInList(m.scoring(), "proportion", "ratio", "continuous-variable")) { 376 if (rule(errors, NO_RULE_DATE, IssueType.REQUIRED, mrg.line(), mrg.col(), stack.getLiteralPath(), ms != null, I18nConstants.MEASURE_MR_SCORE_REQUIRED, m.scoring())) { 377 NodeStack ns = stack.push(ms, -1, ms.getProperty().getDefinition(), ms.getProperty().getDefinition()); 378 Element v = ms.getNamedChild("value"); 379 // TODO: this is a DEQM special and should be handled differently 380 if (v == null) { 381 if (ms.hasExtension("http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/extension-alternateScoreType")) { 382 v = ms.getExtension("http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/extension-alternateScoreType").getNamedChild("value"); 383 } 384 } 385 if ("proportion".equals(m.scoring())) { 386 // proportion - score is a unitless number from 0 ... 1 387 ok = banned(errors, ns, ms, "unit", I18nConstants.MEASURE_MR_SCORE_UNIT_PROHIBITED, "proportion"); 388 ok = banned(errors, ns, ms, "system", I18nConstants.MEASURE_MR_SCORE_UNIT_PROHIBITED, "proportion"); 389 ok = banned(errors, ns, ms, "code", I18nConstants.MEASURE_MR_SCORE_UNIT_PROHIBITED, "proportion"); 390 if (rule(errors, NO_RULE_DATE, IssueType.REQUIRED, ms.line(), ms.col(), ns.getLiteralPath(), v != null, I18nConstants.MEASURE_MR_SCORE_VALUE_REQUIRED, "proportion")) { 391 try { 392 BigDecimal dec = new BigDecimal(v.primitiveValue()); 393 NodeStack nsv = ns.push(v, -1, v.getProperty().getDefinition(), v.getProperty().getDefinition()); 394 ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, v.line(), v.col(), nsv.getLiteralPath(), dec.compareTo(new BigDecimal(0)) >= 0 && dec.compareTo(new BigDecimal(1)) <= 0, I18nConstants.MEASURE_MR_SCORE_VALUE_INVALID_01) && ok; 395 } catch (Exception e) { 396 // nothing - will have caused an error elsewhere 397 } 398 } else { 399 ok = false; 400 } 401 } else if ("ratio".equals(m.scoring())) { 402 // ratio - score is a number with no value constraints, and maybe with a unit (perhaps constrained by extension) 403 if (rule(errors, NO_RULE_DATE, IssueType.REQUIRED, ms.line(), ms.col(), ns.getLiteralPath(), v != null, I18nConstants.MEASURE_MR_SCORE_VALUE_REQUIRED, "ratio")) { 404 Element unit = ms.getNamedChild("code"); 405 Coding c = m.measure().hasExtension(ToolingExtensions.EXT_Q_UNIT) ? (Coding) m.measure().getExtensionByUrl(ToolingExtensions.EXT_Q_UNIT).getValue() : null; 406 if (unit != null) { 407 if (c != null) { 408 NodeStack nsc = ns.push(unit, -1, unit.getProperty().getDefinition(), unit.getProperty().getDefinition()); 409 ok = rule(errors, NO_RULE_DATE, IssueType.CODEINVALID, unit.line(), unit.col(), nsc.getLiteralPath(), c.getCode().equals(unit.primitiveValue()), I18nConstants.MEASURE_MR_SCORE_FIXED, c.getCode()) && ok; 410 Element system = ms.getNamedChild("system"); 411 if (system == null) { 412 NodeStack nss = system == null ? ns : ns.push(system, -1, system.getProperty().getDefinition(), system.getProperty().getDefinition()); 413 ok = rule(errors, NO_RULE_DATE, IssueType.CODEINVALID, system.line(), system.col(), nss.getLiteralPath(), c.getSystem().equals(system.primitiveValue()), I18nConstants.MEASURE_MR_SCORE_FIXED, c.getSystem()) && ok; 414 } else { 415 ok = rule(errors, NO_RULE_DATE, IssueType.CODEINVALID, ms.line(), ms.col(), ns.getLiteralPath(), c.getSystem().equals(system.primitiveValue()), I18nConstants.MEASURE_MR_SCORE_FIXED, c.getSystem()) && ok; 416 } 417 } 418 } else if (c != null) { 419 ok = rule(errors, NO_RULE_DATE, IssueType.NOTFOUND, ms.line(), ms.col(), ns.getLiteralPath(), false, I18nConstants.MEASURE_MR_SCORE_FIXED, DataRenderer.display(context, c)) && ok; 420 } else { 421 warning(errors, NO_RULE_DATE, IssueType.NOTFOUND, ms.line(), ms.col(), ns.getLiteralPath(), false, I18nConstants.MEASURE_MR_SCORE_UNIT_REQUIRED, "ratio"); 422 } 423 } else { 424 ok = true; 425 } 426 } else if ("continuous-variable".equals(m.scoring())) { 427 // continuous-variable - score is a quantity with a unit per the extension 428 if (rule(errors, NO_RULE_DATE, IssueType.REQUIRED, ms.line(), ms.col(), ns.getLiteralPath(), v != null, I18nConstants.MEASURE_MR_SCORE_VALUE_REQUIRED, "continuous-variable")) { 429 Element unit = ms.getNamedChild("code"); 430 Coding c = m.measure().hasExtension(ToolingExtensions.EXT_Q_UNIT) ? (Coding) m.measure().getExtensionByUrl(ToolingExtensions.EXT_Q_UNIT).getValue() : null; 431 if (unit != null) { 432 if (c != null) { 433 NodeStack nsc = ns.push(unit, -1, unit.getProperty().getDefinition(), unit.getProperty().getDefinition()); 434 rule(errors, NO_RULE_DATE, IssueType.CODEINVALID, unit.line(), unit.col(), nsc.getLiteralPath(), c.getCode().equals(unit.primitiveValue()), I18nConstants.MEASURE_MR_SCORE_FIXED, c.getCode()); 435 Element system = ms.getNamedChild("system"); 436 if (system == null) { 437 NodeStack nss = system == null ? ns : ns.push(system, -1, system.getProperty().getDefinition(), system.getProperty().getDefinition()); 438 ok = rule(errors, NO_RULE_DATE, IssueType.CODEINVALID, system.line(), system.col(), nss.getLiteralPath(), c.getSystem().equals(system.primitiveValue()), I18nConstants.MEASURE_MR_SCORE_FIXED, c.getSystem()) && ok; 439 } else { 440 ok = rule(errors, NO_RULE_DATE, IssueType.CODEINVALID, ms.line(), ms.col(), ns.getLiteralPath(), c.getSystem().equals(system.primitiveValue()), I18nConstants.MEASURE_MR_SCORE_FIXED, c.getSystem()) && ok; 441 } 442 } 443 } else if (c != null) { 444 ok = rule(errors, NO_RULE_DATE, IssueType.NOTFOUND, ms.line(), ms.col(), ns.getLiteralPath(), false, I18nConstants.MEASURE_MR_SCORE_FIXED, DataRenderer.display(context, c)) && ok; 445 } 446 } 447 } 448 } else { // else do nothing - there's a hint elsewhere 449 ok = false; 450 } 451 } 452 return ok; 453 } 454 455 private boolean banned(List<ValidationMessage> errors, NodeStack stack, Element parent, String childName, String msgId, Object... params) { 456 Element child = parent.getNamedChild(childName); 457 return banned(errors, stack, child, msgId, params); 458 } 459 460 private boolean banned(List<ValidationMessage> errors, NodeStack stack, Element e, String msgId, Object... params) { 461 if (e != null) { 462 NodeStack ns = stack.push(e, -1, e.getProperty().getDefinition(), e.getProperty().getDefinition()); 463 rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, e.line(), e.col(), ns.getLiteralPath(), false, msgId, params); 464 return false; 465 } else { 466 return true; 467 } 468 } 469 private boolean validateMeasureReportGroupPopulations(ValidatorHostContext hostContext, MeasureContext m, MeasureGroupComponent mg, List<ValidationMessage> errors, Element mrg, NodeStack stack, boolean inProgress) { 470 boolean ok = true; 471 // there must be a population for each population defined in the measure, and no 4others. 472 List<MeasureGroupPopulationComponent> pops = new ArrayList<MeasureGroupPopulationComponent>(); 473 List<Element> plist = mrg.getChildrenByName("population"); 474 475 int i = 0; 476 for (Element mrgp : plist) { 477 NodeStack ns = stack.push(mrgp, i, mrgp.getProperty().getDefinition(), mrgp.getProperty().getDefinition()); 478 CodeableConcept cc = ObjectConverter.readAsCodeableConcept(mrgp.getNamedChild("code")); 479 if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrgp.line(), mrgp.col(), ns.getLiteralPath(), cc != null, I18nConstants.MEASURE_MR_GRP_POP_NO_CODE)) { 480 MeasureGroupPopulationComponent mgp = getGroupPopForCode(cc, mg); 481 if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), mgp != null, I18nConstants.MEASURE_MR_GRP_POP_UNK_CODE)) { 482 if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), !pops.contains(mgp), I18nConstants.MEASURE_MR_GRP_POP_DUPL_CODE)) { 483 pops.add(mgp); 484 ok = validateMeasureReportGroupPopulation(hostContext, m, mgp, errors, mrgp, ns, inProgress) && ok; 485 } else { 486 ok = false; 487 } 488 } else { 489 ok = false; 490 } 491 } else { 492 ok = false; 493 } 494 i++; 495 } 496 for (MeasureGroupPopulationComponent mgp : mg.getPopulation()) { 497 if (!pops.contains(mgp) && !mgp.getCode().hasCoding("http://terminology.hl7.org/CodeSystem/measure-population", "measure-observation")) { 498 ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), stack.getLiteralPath(), pops.contains(mg), I18nConstants.MEASURE_MR_GRP_MISSING_BY_CODE, DataRenderer.display(context, mgp.getCode())) && ok; 499 } 500 } 501 return ok; 502 } 503 504 private boolean validateMeasureReportGroupPopulation(ValidatorHostContext hostContext, MeasureContext m, MeasureGroupPopulationComponent mgp, List<ValidationMessage> errors, Element mrgp, NodeStack ns, boolean inProgress) { 505 boolean ok = true; 506 List<Element> sr = mrgp.getChildrenByName("subjectResults"); 507 if ("subject-list".equals(m.reportType())) { 508 try { 509 int c = Integer.parseInt(mrgp.getChildValue("count")); 510 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; 511 } catch (Exception e) { 512 // nothing; that'll be because count is not valid, and that's a different error or its missing and we don't care 513 } 514 } else { 515 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; 516 warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrgp.line(), mrgp.col(), ns.getLiteralPath(), mrgp.hasChild("count"), I18nConstants.MEASURE_MR_GRP_POP_NO_COUNT); 517 } 518 return ok; 519 } 520 521 private boolean validateMeasureReportGroupStratifiers(ValidatorHostContext hostContext, MeasureContext m, MeasureGroupComponent mg, List<ValidationMessage> errors, Element mrg, NodeStack stack, boolean inProgress) { 522 boolean ok = true; 523 524 // there must be a population for each population defined in the measure, and no 4others. 525 List<MeasureGroupStratifierComponent> strats = new ArrayList<>(); 526 List<Element> slist = mrg.getChildrenByName("stratifier"); 527 528 int i = 0; 529 for (Element mrgs : slist) { 530 NodeStack ns = stack.push(mrgs, i, mrgs.getProperty().getDefinition(), mrgs.getProperty().getDefinition()); 531 CodeableConcept cc = ObjectConverter.readAsCodeableConcept(mrgs.getNamedChild("code")); 532 if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrgs.line(), mrgs.col(), ns.getLiteralPath(), cc != null, I18nConstants.MEASURE_MR_GRP_POP_NO_CODE)) { 533 MeasureGroupStratifierComponent mgs = getGroupStratifierForCode(cc, mg); 534 if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), mgs != null, I18nConstants.MEASURE_MR_GRP_POP_UNK_CODE)) { 535 if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), !strats.contains(mgs), I18nConstants.MEASURE_MR_GRP_POP_DUPL_CODE)) { 536 strats.add(mgs); 537 ok = validateMeasureReportGroupStratifier(hostContext, m, mgs, errors, mrgs, ns, inProgress) && ok; 538 } else { 539 ok = false; 540 } 541 } else { 542 ok = false; 543 } 544 } else { 545 ok = false; 546 } 547 i++; 548 } 549 for (MeasureGroupStratifierComponent mgs : mg.getStratifier()) { 550 if (!strats.contains(mgs)) { 551 ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), stack.getLiteralPath(), strats.contains(mg), I18nConstants.MEASURE_MR_GRP_MISSING_BY_CODE, DataRenderer.display(context, mgs.getCode())) && ok; 552 } 553 } 554 return true; 555 } 556 557 private boolean validateMeasureReportGroupStratifier(ValidatorHostContext hostContext, MeasureContext m, MeasureGroupStratifierComponent mgs, List<ValidationMessage> errors, Element mrgs, NodeStack ns, boolean inProgress) { 558 // still to be done 559 return true; 560 } 561 562 private MeasureGroupStratifierComponent getGroupStratifierForCode(CodeableConcept cc, MeasureGroupComponent mg) { 563 for (MeasureGroupStratifierComponent t : mg.getStratifier()) { 564 if (t.hasCode()) { 565 for (Coding c : t.getCode().getCoding()) { 566 if (cc.hasCoding(c.getSystem(), c.getCode())) { 567 return t; 568 } 569 } 570 if (!cc.hasCoding() && !t.getCode().hasCoding()) { 571 if (cc.hasText() && t.getCode().hasText()) { 572 if (cc.getText().equals(t.getCode().getText())) { 573 return t; 574 } 575 } 576 } 577 } 578 } 579 return null; 580 } 581 582 private boolean hasUseableCode(CodeableConcept cc) { 583 for (Coding c : cc.getCoding()) { 584 if (c.hasSystem() && c.hasCode()) { 585 return true; 586 } 587 } 588 return false; 589 } 590 591 private MeasureGroupPopulationComponent getGroupPopForCode(CodeableConcept cc, MeasureGroupComponent mg) { 592 for (MeasureGroupPopulationComponent t : mg.getPopulation()) { 593 if (t.hasCode()) { 594 for (Coding c : t.getCode().getCoding()) { 595 if (cc.hasCoding(c.getSystem(), c.getCode())) { 596 return t; 597 } 598 } 599 } 600 } 601 return null; 602 } 603 private MeasureGroupComponent getGroupForCode(CodeableConcept cc, Measure m) { 604 for (MeasureGroupComponent t : m.getGroup()) { 605 if (t.hasCode()) { 606 for (Coding c : t.getCode().getCoding()) { 607 if (cc.hasCoding(c.getSystem(), c.getCode())) { 608 return t; 609 } 610 } 611 } 612 } 613 return null; 614 } 615 616 617}