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