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}