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