001package ca.uhn.fhir.cql.r4.evaluation; 002 003/*- 004 * #%L 005 * HAPI FHIR JPA Server - Clinical Quality Language 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.cql.common.evaluation.MeasurePopulationType; 025import ca.uhn.fhir.cql.common.evaluation.MeasureScoring; 026import ca.uhn.fhir.cql.r4.builder.MeasureReportBuilder; 027import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 028import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 029import ca.uhn.fhir.rest.api.server.IBundleProvider; 030import ca.uhn.fhir.rest.api.server.RequestDetails; 031import ca.uhn.fhir.rest.param.ReferenceParam; 032import org.cqframework.cql.elm.execution.ExpressionDef; 033import org.cqframework.cql.elm.execution.FunctionDef; 034import org.hl7.fhir.instance.model.api.IBaseResource; 035import org.hl7.fhir.r4.model.CanonicalType; 036import org.hl7.fhir.r4.model.CodeableConcept; 037import org.hl7.fhir.r4.model.Coding; 038import org.hl7.fhir.r4.model.Extension; 039import org.hl7.fhir.r4.model.IdType; 040import org.hl7.fhir.r4.model.ListResource; 041import org.hl7.fhir.r4.model.Measure; 042import org.hl7.fhir.r4.model.MeasureReport; 043import org.hl7.fhir.r4.model.Observation; 044import org.hl7.fhir.r4.model.Patient; 045import org.hl7.fhir.r4.model.Quantity; 046import org.hl7.fhir.r4.model.Reference; 047import org.hl7.fhir.r4.model.Resource; 048import org.hl7.fhir.r4.model.StringType; 049import org.opencds.cqf.cql.engine.data.DataProvider; 050import org.opencds.cqf.cql.engine.execution.Context; 051import org.opencds.cqf.cql.engine.execution.Variable; 052import org.opencds.cqf.cql.engine.runtime.Code; 053import org.opencds.cqf.cql.engine.runtime.Interval; 054import org.slf4j.Logger; 055import org.slf4j.LoggerFactory; 056 057import java.lang.reflect.Field; 058import java.util.ArrayList; 059import java.util.Collections; 060import java.util.HashMap; 061import java.util.HashSet; 062import java.util.LinkedHashMap; 063import java.util.List; 064import java.util.Set; 065import java.util.UUID; 066import java.util.stream.Collectors; 067 068public class MeasureEvaluation { 069 070 private static final Logger logger = LoggerFactory.getLogger(MeasureEvaluation.class); 071 072 private final DataProvider provider; 073 private final Interval measurementPeriod; 074 private final DaoRegistry registry; 075 076 public MeasureEvaluation(DataProvider provider, DaoRegistry registry, Interval measurementPeriod) { 077 this.provider = provider; 078 this.registry = registry; 079 this.measurementPeriod = measurementPeriod; 080 } 081 082 public MeasureReport evaluatePatientMeasure(Measure measure, Context context, String patientId, RequestDetails theRequestDetails) { 083 logger.info("Generating individual report"); 084 085 if (patientId == null) { 086 return evaluatePopulationMeasure(measure, context, theRequestDetails); 087 } 088 089 Iterable<Object> patientRetrieve = provider.retrieve("Patient", "id", patientId, "Patient", null, null, null, 090 null, null, null, null, null); 091 Patient patient = null; 092 if (patientRetrieve.iterator().hasNext()) { 093 patient = (Patient) patientRetrieve.iterator().next(); 094 } 095 096 boolean isSingle = true; 097 return evaluate(measure, context, patient == null ? Collections.emptyList() : Collections.singletonList(patient), 098 MeasureReport.MeasureReportType.INDIVIDUAL, isSingle); 099 } 100 101 public MeasureReport evaluateSubjectListMeasure(Measure measure, Context context, String practitionerRef, RequestDetails theRequestDetails) { 102 logger.info("Generating patient-list report"); 103 104 List<Patient> patients = practitionerRef == null ? getAllPatients(theRequestDetails) : getPractitionerPatients(practitionerRef, theRequestDetails); 105 boolean isSingle = false; 106 return evaluate(measure, context, patients, MeasureReport.MeasureReportType.SUBJECTLIST, isSingle); 107 } 108 109 private List<Patient> getPractitionerPatients(String practitionerRef, RequestDetails theRequestDetails) { 110 SearchParameterMap map = SearchParameterMap.newSynchronous(); 111 map.add("general-practitioner", new ReferenceParam( 112 practitionerRef.startsWith("Practitioner/") ? practitionerRef : "Practitioner/" + practitionerRef)); 113 114 List<Patient> patients = new ArrayList<>(); 115 IBundleProvider patientProvider = registry.getResourceDao("Patient").search(map, theRequestDetails); 116 List<IBaseResource> patientList = patientProvider.getAllResources(); 117 patientList.forEach(x -> patients.add((Patient) x)); 118 return patients; 119 } 120 121 private List<Patient> getAllPatients(RequestDetails theRequestDetails) { 122 List<Patient> patients = new ArrayList<>(); 123 IBundleProvider patientProvider = registry.getResourceDao("Patient").search(SearchParameterMap.newSynchronous(), theRequestDetails); 124 List<IBaseResource> patientList = patientProvider.getAllResources(); 125 patientList.forEach(x -> patients.add((Patient) x)); 126 return patients; 127 } 128 129 public MeasureReport evaluatePopulationMeasure(Measure measure, Context context, RequestDetails theRequestDetails) { 130 logger.info("Generating summary report"); 131 132 boolean isSingle = false; 133 return evaluate(measure, context, getAllPatients(theRequestDetails), MeasureReport.MeasureReportType.SUMMARY, isSingle); 134 } 135 136 @SuppressWarnings("unchecked") 137 private void clearExpressionCache(Context context) { 138 // Hack to clear expression cache 139 // See cqf-ruler github issue #153 140 try { 141 Field privateField = Context.class.getDeclaredField("expressions"); 142 privateField.setAccessible(true); 143 LinkedHashMap<String, Object> expressions = (LinkedHashMap<String, Object>) privateField.get(context); 144 expressions.clear(); 145 146 } catch (Exception e) { 147 logger.warn("Error resetting expression cache", e); 148 } 149 } 150 151 private Resource evaluateObservationCriteria(Context context, Patient patient, Resource resource, 152 Measure.MeasureGroupPopulationComponent pop, MeasureReport report) { 153 if (pop == null || !pop.hasCriteria()) { 154 return null; 155 } 156 157 context.setContextValue("Patient", patient.getIdElement().getIdPart()); 158 159 clearExpressionCache(context); 160 161 String observationName = pop.getCriteria().getExpression(); 162 ExpressionDef ed = context.resolveExpressionRef(observationName); 163 if (!(ed instanceof FunctionDef)) { 164 throw new IllegalArgumentException(Msg.code(1672) + String.format("Measure observation %s does not reference a function definition", observationName)); 165 } 166 167 Object result = null; 168 context.pushWindow(); 169 try { 170 context.push(new Variable().withName(((FunctionDef) ed).getOperand().get(0).getName()).withValue(resource)); 171 result = ed.getExpression().evaluate(context); 172 } finally { 173 context.popWindow(); 174 } 175 176 if (result instanceof Resource) { 177 return (Resource) result; 178 } 179 180 Observation obs = new Observation(); 181 obs.setStatus(Observation.ObservationStatus.FINAL); 182 obs.setId(UUID.randomUUID().toString()); 183 CodeableConcept cc = new CodeableConcept(); 184 cc.setText(observationName); 185 obs.setCode(cc); 186 Extension obsExtension = new Extension().setUrl("http://hl7.org/fhir/StructureDefinition/cqf-measureInfo"); 187 Extension extExtMeasure = new Extension().setUrl("measure") 188 .setValue(new CanonicalType("http://hl7.org/fhir/us/cqfmeasures/" + report.getMeasure())); 189 obsExtension.addExtension(extExtMeasure); 190 Extension extExtPop = new Extension().setUrl("populationId").setValue(new StringType(observationName)); 191 obsExtension.addExtension(extExtPop); 192 obs.addExtension(obsExtension); 193 return obs; 194 } 195 196 @SuppressWarnings("unchecked") 197 private Iterable<Resource> evaluateCriteria(Context context, Patient patient, 198 Measure.MeasureGroupPopulationComponent pop) { 199 if (pop == null || !pop.hasCriteria()) { 200 return Collections.emptyList(); 201 } 202 203 context.setContextValue("Patient", patient.getIdElement().getIdPart()); 204 205 clearExpressionCache(context); 206 207 Object result = context.resolveExpressionRef(pop.getCriteria().getExpression()).evaluate(context); 208 if (result == null) { 209 return Collections.emptyList(); 210 } 211 212 if (result instanceof Boolean) { 213 if (((Boolean) result)) { 214 return Collections.singletonList(patient); 215 } else { 216 return Collections.emptyList(); 217 } 218 } 219 220 return (Iterable<Resource>) result; 221 } 222 223 private boolean evaluatePopulationCriteria(Context context, Patient patient, 224 Measure.MeasureGroupPopulationComponent criteria, HashMap<String, Resource> population, 225 HashMap<String, Patient> populationPatients, Measure.MeasureGroupPopulationComponent exclusionCriteria, 226 HashMap<String, Resource> exclusionPopulation, HashMap<String, Patient> exclusionPatients) { 227 boolean inPopulation = false; 228 if (criteria != null) { 229 for (Resource resource : evaluateCriteria(context, patient, criteria)) { 230 inPopulation = true; 231 population.put(resource.getIdElement().getIdPart(), resource); 232 } 233 } 234 235 if (inPopulation) { 236 // Are they in the exclusion? 237 if (exclusionCriteria != null) { 238 for (Resource resource : evaluateCriteria(context, patient, exclusionCriteria)) { 239 inPopulation = false; 240 exclusionPopulation.put(resource.getIdElement().getIdPart(), resource); 241 population.remove(resource.getIdElement().getIdPart()); 242 } 243 } 244 } 245 246 if (inPopulation && populationPatients != null) { 247 populationPatients.put(patient.getIdElement().getIdPart(), patient); 248 } 249 if (!inPopulation && exclusionPatients != null) { 250 exclusionPatients.put(patient.getIdElement().getIdPart(), patient); 251 } 252 253 return inPopulation; 254 } 255 256 private void addPopulationCriteriaReport(MeasureReport report, MeasureReport.MeasureReportGroupComponent reportGroup, 257 Measure.MeasureGroupPopulationComponent populationCriteria, int populationCount, 258 Iterable<Patient> patientPopulation) { 259 if (populationCriteria != null) { 260 MeasureReport.MeasureReportGroupPopulationComponent populationReport = new MeasureReport.MeasureReportGroupPopulationComponent(); 261 populationReport.setCode(populationCriteria.getCode()); 262 if (report.getType() == MeasureReport.MeasureReportType.SUBJECTLIST && patientPopulation != null) { 263 ListResource SUBJECTLIST = new ListResource(); 264 SUBJECTLIST.setId(UUID.randomUUID().toString()); 265 populationReport.setSubjectResults(new Reference().setReference("#" + SUBJECTLIST.getId())); 266 for (Patient patient : patientPopulation) { 267 ListResource.ListEntryComponent entry = new ListResource.ListEntryComponent().setItem(new Reference() 268 .setReference(patient.getIdElement().getIdPart().startsWith("Patient/") 269 ? patient.getIdElement().getIdPart() 270 : String.format("Patient/%s", patient.getIdElement().getIdPart())) 271 .setDisplay(patient.getNameFirstRep().getNameAsSingleString())); 272 SUBJECTLIST.addEntry(entry); 273 } 274 report.addContained(SUBJECTLIST); 275 } 276 populationReport.setCount(populationCount); 277 reportGroup.addPopulation(populationReport); 278 } 279 } 280 281 private MeasureReport evaluate(Measure measure, Context context, List<Patient> patients, 282 MeasureReport.MeasureReportType type, boolean isSingle) { 283 MeasureReportBuilder reportBuilder = new MeasureReportBuilder(); 284 reportBuilder.buildStatus("complete"); 285 reportBuilder.buildType(type); 286 reportBuilder 287 .buildMeasureReference(measure.getIdElement().getResourceType() + "/" + measure.getIdElement().getIdPart()); 288 if (type == MeasureReport.MeasureReportType.INDIVIDUAL && !patients.isEmpty()) { 289 IdType patientId = patients.get(0).getIdElement(); 290 reportBuilder.buildPatientReference(patientId.getResourceType() + "/" + patientId.getIdPart()); 291 } 292 if (measurementPeriod != null) { 293 reportBuilder.buildPeriod(measurementPeriod); 294 } 295 296 MeasureReport report = reportBuilder.build(); 297 298 HashMap<String, Resource> resources = new HashMap<>(); 299 HashMap<String, HashSet<String>> codeToResourceMap = new HashMap<>(); 300 Set<String> evaluatedResourcesList = new HashSet<>(); 301 302 MeasureScoring measureScoring = MeasureScoring.fromCode(measure.getScoring().getCodingFirstRep().getCode()); 303 if (measureScoring == null) { 304 throw new RuntimeException(Msg.code(1673) + "Measure scoring is required in order to calculate."); 305 } 306 307 List<Measure.MeasureSupplementalDataComponent> sde = new ArrayList<>(); 308 HashMap<String, HashMap<String, Integer>> sdeAccumulators = null; 309 for (Measure.MeasureGroupComponent group : measure.getGroup()) { 310 MeasureReport.MeasureReportGroupComponent reportGroup = new MeasureReport.MeasureReportGroupComponent(); 311 reportGroup.setId(group.getId()); 312 report.getGroup().add(reportGroup); 313 314 // Declare variables to avoid a hash lookup on every patient 315 // TODO: Isn't quite right, there may be multiple initial populations for a 316 // ratio measure... 317 Measure.MeasureGroupPopulationComponent initialPopulationCriteria = null; 318 Measure.MeasureGroupPopulationComponent numeratorCriteria = null; 319 Measure.MeasureGroupPopulationComponent numeratorExclusionCriteria = null; 320 Measure.MeasureGroupPopulationComponent denominatorCriteria = null; 321 Measure.MeasureGroupPopulationComponent denominatorExclusionCriteria = null; 322 Measure.MeasureGroupPopulationComponent denominatorExceptionCriteria = null; 323 Measure.MeasureGroupPopulationComponent measurePopulationCriteria = null; 324 Measure.MeasureGroupPopulationComponent measurePopulationExclusionCriteria = null; 325 // TODO: Isn't quite right, there may be multiple measure observations... 326 Measure.MeasureGroupPopulationComponent measureObservationCriteria = null; 327 328 HashMap<String, Resource> initialPopulation = null; 329 HashMap<String, Resource> numerator = null; 330 HashMap<String, Resource> numeratorExclusion = null; 331 HashMap<String, Resource> denominator = null; 332 HashMap<String, Resource> denominatorExclusion = null; 333 HashMap<String, Resource> denominatorException = null; 334 HashMap<String, Resource> measurePopulation = null; 335 HashMap<String, Resource> measurePopulationExclusion = null; 336 HashMap<String, Resource> measureObservation = null; 337 338 HashMap<String, Patient> initialPopulationPatients = null; 339 HashMap<String, Patient> numeratorPatients = null; 340 HashMap<String, Patient> numeratorExclusionPatients = null; 341 HashMap<String, Patient> denominatorPatients = null; 342 HashMap<String, Patient> denominatorExclusionPatients = null; 343 HashMap<String, Patient> denominatorExceptionPatients = null; 344 HashMap<String, Patient> measurePopulationPatients = null; 345 HashMap<String, Patient> measurePopulationExclusionPatients = null; 346 347 sdeAccumulators = new HashMap<>(); 348 sde = measure.getSupplementalData(); 349 for (Measure.MeasureGroupPopulationComponent pop : group.getPopulation()) { 350 MeasurePopulationType populationType = MeasurePopulationType 351 .fromCode(pop.getCode().getCodingFirstRep().getCode()); 352 if (populationType != null) { 353 switch (populationType) { 354 case INITIALPOPULATION: 355 initialPopulationCriteria = pop; 356 initialPopulation = new HashMap<>(); 357 if (type == MeasureReport.MeasureReportType.SUBJECTLIST) { 358 initialPopulationPatients = new HashMap<>(); 359 } 360 break; 361 case NUMERATOR: 362 numeratorCriteria = pop; 363 numerator = new HashMap<>(); 364 if (type == MeasureReport.MeasureReportType.SUBJECTLIST) { 365 numeratorPatients = new HashMap<>(); 366 } 367 break; 368 case NUMERATOREXCLUSION: 369 numeratorExclusionCriteria = pop; 370 numeratorExclusion = new HashMap<>(); 371 if (type == MeasureReport.MeasureReportType.SUBJECTLIST) { 372 numeratorExclusionPatients = new HashMap<>(); 373 } 374 break; 375 case DENOMINATOR: 376 denominatorCriteria = pop; 377 denominator = new HashMap<>(); 378 if (type == MeasureReport.MeasureReportType.SUBJECTLIST) { 379 denominatorPatients = new HashMap<>(); 380 } 381 break; 382 case DENOMINATOREXCLUSION: 383 denominatorExclusionCriteria = pop; 384 denominatorExclusion = new HashMap<>(); 385 if (type == MeasureReport.MeasureReportType.SUBJECTLIST) { 386 denominatorExclusionPatients = new HashMap<>(); 387 } 388 break; 389 case DENOMINATOREXCEPTION: 390 denominatorExceptionCriteria = pop; 391 denominatorException = new HashMap<>(); 392 if (type == MeasureReport.MeasureReportType.SUBJECTLIST) { 393 denominatorExceptionPatients = new HashMap<>(); 394 } 395 break; 396 case MEASUREPOPULATION: 397 measurePopulationCriteria = pop; 398 measurePopulation = new HashMap<>(); 399 if (type == MeasureReport.MeasureReportType.SUBJECTLIST) { 400 measurePopulationPatients = new HashMap<>(); 401 } 402 break; 403 case MEASUREPOPULATIONEXCLUSION: 404 measurePopulationExclusionCriteria = pop; 405 measurePopulationExclusion = new HashMap<>(); 406 if (type == MeasureReport.MeasureReportType.SUBJECTLIST) { 407 measurePopulationExclusionPatients = new HashMap<>(); 408 } 409 break; 410 case MEASUREOBSERVATION: 411 measureObservationCriteria = pop; 412 measureObservation = new HashMap<>(); 413 break; 414 } 415 } 416 } 417 418 switch (measureScoring) { 419 case PROPORTION: 420 case RATIO: { 421 422 // For each patient in the initial population 423 for (Patient patient : patients) { 424 // Are they in the initial population? 425 boolean inInitialPopulation = evaluatePopulationCriteria(context, patient, initialPopulationCriteria, 426 initialPopulation, initialPopulationPatients, null, null, null); 427 populateResourceMap(context, MeasurePopulationType.INITIALPOPULATION, resources, codeToResourceMap); 428 429 if (inInitialPopulation) { 430 // Are they in the denominator? 431 boolean inDenominator = evaluatePopulationCriteria(context, patient, denominatorCriteria, denominator, 432 denominatorPatients, denominatorExclusionCriteria, denominatorExclusion, 433 denominatorExclusionPatients); 434 populateResourceMap(context, MeasurePopulationType.DENOMINATOR, resources, codeToResourceMap); 435 436 if (inDenominator) { 437 // Are they in the numerator? 438 boolean inNumerator = evaluatePopulationCriteria(context, patient, numeratorCriteria, numerator, 439 numeratorPatients, numeratorExclusionCriteria, numeratorExclusion, 440 numeratorExclusionPatients); 441 populateResourceMap(context, MeasurePopulationType.NUMERATOR, resources, codeToResourceMap); 442 443 if (!inNumerator && inDenominator && (denominatorExceptionCriteria != null)) { 444 // Are they in the denominator exception? 445 boolean inException = false; 446 for (Resource resource : evaluateCriteria(context, patient, denominatorExceptionCriteria)) { 447 inException = true; 448 denominatorException.put(resource.getIdElement().getIdPart(), resource); 449 denominator.remove(resource.getIdElement().getIdPart()); 450 populateResourceMap(context, MeasurePopulationType.DENOMINATOREXCEPTION, resources, 451 codeToResourceMap); 452 } 453 if (inException) { 454 if (denominatorExceptionPatients != null) { 455 denominatorExceptionPatients.put(patient.getIdElement().getIdPart(), patient); 456 } 457 if (denominatorPatients != null) { 458 denominatorPatients.remove(patient.getIdElement().getIdPart()); 459 } 460 } 461 } 462 } 463 } 464 populateSDEAccumulators(measure, context, patient, sdeAccumulators, sde); 465 } 466 467 // Calculate actual measure score, Count(numerator) / Count(denominator) 468 if (denominator != null && numerator != null && denominator.size() > 0) { 469 reportGroup.setMeasureScore(new Quantity(numerator.size() / (double) denominator.size())); 470 } 471 472 break; 473 } 474 case CONTINUOUSVARIABLE: { 475 476 // For each patient in the patient list 477 for (Patient patient : patients) { 478 479 // Are they in the initial population? 480 boolean inInitialPopulation = evaluatePopulationCriteria(context, patient, initialPopulationCriteria, 481 initialPopulation, initialPopulationPatients, null, null, null); 482 populateResourceMap(context, MeasurePopulationType.INITIALPOPULATION, resources, codeToResourceMap); 483 484 if (inInitialPopulation) { 485 // Are they in the measure population? 486 boolean inMeasurePopulation = evaluatePopulationCriteria(context, patient, measurePopulationCriteria, 487 measurePopulation, measurePopulationPatients, measurePopulationExclusionCriteria, 488 measurePopulationExclusion, measurePopulationExclusionPatients); 489 490 if (inMeasurePopulation) { 491 for (Resource resource : measurePopulation.values()) { 492 Resource observation = evaluateObservationCriteria(context, patient, resource, 493 measureObservationCriteria, report); 494 measureObservation.put(resource.getIdElement().getIdPart(), observation); 495 report.addContained(observation); 496 report.getEvaluatedResource().add(new Reference("#" + observation.getId())); 497 } 498 } 499 } 500 populateSDEAccumulators(measure, context, patient, sdeAccumulators, sde); 501 } 502 503 break; 504 } 505 case COHORT: { 506 507 // For each patient in the patient list 508 for (Patient patient : patients) { 509 evaluatePopulationCriteria(context, patient, initialPopulationCriteria, initialPopulation, 510 initialPopulationPatients, null, null, null); 511 populateResourceMap(context, MeasurePopulationType.INITIALPOPULATION, resources, codeToResourceMap); 512 populateSDEAccumulators(measure, context, patient, sdeAccumulators, sde); 513 } 514 515 break; 516 } 517 } 518 519 // Add population reports for each group 520 addPopulationCriteriaReport(report, reportGroup, initialPopulationCriteria, 521 initialPopulation != null ? initialPopulation.size() : 0, 522 initialPopulationPatients != null ? initialPopulationPatients.values() : null); 523 addPopulationCriteriaReport(report, reportGroup, numeratorCriteria, numerator != null ? numerator.size() : 0, 524 numeratorPatients != null ? numeratorPatients.values() : null); 525 addPopulationCriteriaReport(report, reportGroup, numeratorExclusionCriteria, 526 numeratorExclusion != null ? numeratorExclusion.size() : 0, 527 numeratorExclusionPatients != null ? numeratorExclusionPatients.values() : null); 528 addPopulationCriteriaReport(report, reportGroup, denominatorCriteria, 529 denominator != null ? denominator.size() : 0, 530 denominatorPatients != null ? denominatorPatients.values() : null); 531 addPopulationCriteriaReport(report, reportGroup, denominatorExclusionCriteria, 532 denominatorExclusion != null ? denominatorExclusion.size() : 0, 533 denominatorExclusionPatients != null ? denominatorExclusionPatients.values() : null); 534 addPopulationCriteriaReport(report, reportGroup, denominatorExceptionCriteria, 535 denominatorException != null ? denominatorException.size() : 0, 536 denominatorExceptionPatients != null ? denominatorExceptionPatients.values() : null); 537 addPopulationCriteriaReport(report, reportGroup, measurePopulationCriteria, 538 measurePopulation != null ? measurePopulation.size() : 0, 539 measurePopulationPatients != null ? measurePopulationPatients.values() : null); 540 addPopulationCriteriaReport(report, reportGroup, measurePopulationExclusionCriteria, 541 measurePopulationExclusion != null ? measurePopulationExclusion.size() : 0, 542 measurePopulationExclusionPatients != null ? measurePopulationExclusionPatients.values() : null); 543 // TODO: Measure Observations... 544 } 545 546 for (String key : codeToResourceMap.keySet()) { 547 org.hl7.fhir.r4.model.ListResource list = new org.hl7.fhir.r4.model.ListResource(); 548 for (String element : codeToResourceMap.get(key)) { 549 org.hl7.fhir.r4.model.ListResource.ListEntryComponent comp = new org.hl7.fhir.r4.model.ListResource.ListEntryComponent(); 550 comp.setItem(new Reference('#' + element)); 551 list.addEntry(comp); 552 } 553 554 if (!list.isEmpty()) { 555 list.setId("List/" + UUID.randomUUID()); 556 list.setTitle(key); 557 resources.put(list.getId(), list); 558 list.getEntry().forEach(listResource -> evaluatedResourcesList.add(listResource.getItem().getReference())); 559 } 560 } 561 562 if (!evaluatedResourcesList.isEmpty()) { 563 List<Reference> evaluatedResourceIds = new ArrayList<>(); 564 evaluatedResourcesList.forEach((resource) -> { 565 evaluatedResourceIds.add(new Reference(resource)); 566 }); 567 report.setEvaluatedResource(evaluatedResourceIds); 568 } 569 570 if (sdeAccumulators.size() > 0) { 571 report = processAccumulators(report, sdeAccumulators, sde, isSingle, patients); 572 } 573 574 return report; 575 } 576 577 private void populateSDEAccumulators(Measure measure, Context context, Patient patient, 578 HashMap<String, HashMap<String, Integer>> sdeAccumulators, 579 List<Measure.MeasureSupplementalDataComponent> sde) { 580 context.setContextValue("Patient", patient.getIdElement().getIdPart()); 581 List<Object> sdeList = sde.stream() 582 .map(sdeItem -> context.resolveExpressionRef(sdeItem.getCriteria().getExpression()).evaluate(context)) 583 .collect(Collectors.toList()); 584 if (!sdeList.isEmpty()) { 585 for (int i = 0; i < sdeList.size(); i++) { 586 Object sdeListItem = sdeList.get(i); 587 if (null != sdeListItem) { 588 String sdeAccumulatorKey = sde.get(i).getCode().getText(); 589 if (null == sdeAccumulatorKey || sdeAccumulatorKey.length() < 1) { 590 sdeAccumulatorKey = sde.get(i).getCriteria().getExpression(); 591 } 592 HashMap<String, Integer> sdeItemMap = sdeAccumulators.get(sdeAccumulatorKey); 593 String code = ""; 594 595 switch (sdeListItem.getClass().getSimpleName()) { 596 case "Code": 597 code = ((Code) sdeListItem).getCode(); 598 break; 599 case "ArrayList": 600 if (((ArrayList<?>) sdeListItem).size() > 0) { 601 if (((ArrayList<?>) sdeListItem).get(0).getClass().getSimpleName().equals("Coding")) { 602 code = ((Coding) ((ArrayList<?>) sdeListItem).get(0)).getCode(); 603 } else { 604 continue; 605 } 606 } else { 607 continue; 608 } 609 break; 610 } 611 if (null == code) { 612 continue; 613 } 614 if (null != sdeItemMap && null != sdeItemMap.get(code)) { 615 Integer sdeItemValue = sdeItemMap.get(code); 616 sdeItemValue++; 617 sdeItemMap.put(code, sdeItemValue); 618 sdeAccumulators.get(sdeAccumulatorKey).put(code, sdeItemValue); 619 } else { 620 if (null == sdeAccumulators.get(sdeAccumulatorKey)) { 621 HashMap<String, Integer> newSDEItem = new HashMap<>(); 622 newSDEItem.put(code, 1); 623 sdeAccumulators.put(sdeAccumulatorKey, newSDEItem); 624 } else { 625 sdeAccumulators.get(sdeAccumulatorKey).put(code, 1); 626 } 627 } 628 } 629 } 630 } 631 } 632 633 private MeasureReport processAccumulators(MeasureReport report, 634 HashMap<String, HashMap<String, Integer>> sdeAccumulators, List<Measure.MeasureSupplementalDataComponent> sde, 635 boolean isSingle, List<Patient> patients) { 636 List<Reference> newRefList = new ArrayList<>(); 637 sdeAccumulators.forEach((sdeKey, sdeAccumulator) -> { 638 sdeAccumulator.forEach((sdeAccumulatorKey, sdeAccumulatorValue) -> { 639 Observation obs = new Observation(); 640 obs.setStatus(Observation.ObservationStatus.FINAL); 641 obs.setId(UUID.randomUUID().toString()); 642 Coding valueCoding = new Coding(); 643 if (sdeKey.equalsIgnoreCase("sde-sex")) { 644 valueCoding.setCode(sdeAccumulatorKey); 645 } else { 646 String coreCategory = sdeKey.substring(sdeKey.lastIndexOf('-') >= 0 ? sdeKey.lastIndexOf('-') : 0); 647 patients.forEach((pt) -> { 648 pt.getExtension().forEach((ptExt) -> { 649 if (ptExt.getUrl().contains(coreCategory)) { 650 String code = ((Coding) ptExt.getExtension().get(0).getValue()).getCode(); 651 if (code.equalsIgnoreCase(sdeAccumulatorKey)) { 652 valueCoding.setSystem(((Coding) ptExt.getExtension().get(0).getValue()).getSystem()); 653 valueCoding.setCode(code); 654 valueCoding.setDisplay(((Coding) ptExt.getExtension().get(0).getValue()).getDisplay()); 655 } 656 } 657 }); 658 }); 659 } 660 CodeableConcept obsCodeableConcept = new CodeableConcept(); 661 Extension obsExtension = new Extension().setUrl("http://hl7.org/fhir/StructureDefinition/cqf-measureInfo"); 662 Extension extExtMeasure = new Extension().setUrl("measure") 663 .setValue(new CanonicalType("http://hl7.org/fhir/us/cqfmeasures/" + report.getMeasure())); 664 obsExtension.addExtension(extExtMeasure); 665 Extension extExtPop = new Extension().setUrl("populationId").setValue(new StringType(sdeKey)); 666 obsExtension.addExtension(extExtPop); 667 obs.addExtension(obsExtension); 668 obs.setValue(new Quantity(sdeAccumulatorValue)); 669 if (!isSingle) { 670 valueCoding.setCode(sdeAccumulatorKey); 671 obsCodeableConcept.setCoding(Collections.singletonList(valueCoding)); 672 obs.setCode(obsCodeableConcept); 673 } else { 674 obs.setCode(new CodeableConcept().setText(sdeKey)); 675 obsCodeableConcept.setCoding(Collections.singletonList(valueCoding)); 676 obs.setValue(obsCodeableConcept); 677 } 678 newRefList.add(new Reference("#" + obs.getId())); 679 report.addContained(obs); 680 }); 681 }); 682 newRefList.addAll(report.getEvaluatedResource()); 683 report.setEvaluatedResource(newRefList); 684 return report; 685 } 686 687 private void populateResourceMap(Context context, MeasurePopulationType type, HashMap<String, Resource> resources, 688 HashMap<String, HashSet<String>> codeToResourceMap) { 689 if (context.getEvaluatedResources().isEmpty()) { 690 return; 691 } 692 693 if (!codeToResourceMap.containsKey(type.toCode())) { 694 codeToResourceMap.put(type.toCode(), new HashSet<>()); 695 } 696 697 HashSet<String> codeHashSet = codeToResourceMap.get((type.toCode())); 698 699 for (Object o : context.getEvaluatedResources()) { 700 if (o instanceof Resource) { 701 Resource r = (Resource) o; 702 String id = (r.getIdElement().getResourceType() != null ? (r.getIdElement().getResourceType() + "/") : "") 703 + r.getIdElement().getIdPart(); 704 codeHashSet.add(id); 705 706 if (!resources.containsKey(id)) { 707 resources.put(id, r); 708 } 709 } 710 } 711 712 context.clearEvaluatedResources(); 713 } 714}