Skip to content

Commit

Permalink
multi-measure feature enhancements (#491)
Browse files Browse the repository at this point in the history
* consolidate getMeasures request

* fix mapping and care-gaps test

* commented out stubbed in identifier logic due to testing coverage score
  • Loading branch information
Capt-Mac authored Jul 23, 2024
1 parent 92fdbfb commit f196719
Show file tree
Hide file tree
Showing 8 changed files with 629 additions and 149 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
Expand All @@ -49,14 +50,15 @@
import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.PrimitiveType;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource;
import org.opencds.cqf.fhir.api.Repository;
import org.opencds.cqf.fhir.cr.measure.CareGapsProperties;
import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions;
import org.opencds.cqf.fhir.cr.measure.common.MeasureReportType;
import org.opencds.cqf.fhir.cr.measure.enumeration.CareGapsStatusCode;
import org.opencds.cqf.fhir.utility.Canonicals;
import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils;
import org.opencds.cqf.fhir.utility.Ids;
import org.opencds.cqf.fhir.utility.Resources;
import org.opencds.cqf.fhir.utility.builder.BundleBuilder;
Expand All @@ -66,7 +68,6 @@
import org.opencds.cqf.fhir.utility.builder.DetectedIssueBuilder;
import org.opencds.cqf.fhir.utility.builder.NarrativeSettings;
import org.opencds.cqf.fhir.utility.monad.Eithers;
import org.opencds.cqf.fhir.utility.search.Searches;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -92,6 +93,8 @@ public class R4CareGapsService {

protected final Map<String, Resource> configuredResources = new HashMap<>();

private final R4MeasureServiceUtils r4MeasureServiceUtils;

public R4CareGapsService(
CareGapsProperties careGapsProperties,
Repository repository,
Expand All @@ -101,6 +104,8 @@ public R4CareGapsService(
this.careGapsProperties = careGapsProperties;
this.measureEvaluationOptions = measureEvaluationOptions;
this.serverBase = serverBase;

r4MeasureServiceUtils = new R4MeasureServiceUtils(repository);
}

/**
Expand Down Expand Up @@ -135,7 +140,14 @@ public Parameters getCareGapsReport(

validateConfiguration();

List<Measure> measures = ensureMeasures(getMeasures(measureIds, measureIdentifiers, measureUrls));
List<Measure> measures = ensureMeasures(r4MeasureServiceUtils.getMeasures(
measureIds.stream().map(IdType::new).collect(Collectors.toList()),
measureIdentifiers,
measureUrls.stream()
.map(PrimitiveType::toString)
.map(x -> x.replace("CanonicalType[", ""))
.map(x -> x.replace("]", ""))
.collect(Collectors.toList())));

List<Patient> patients;
if (!Strings.isNullOrEmpty(subject)) {
Expand Down Expand Up @@ -256,54 +268,6 @@ protected Patient validatePatientExists(String patientRef) {
return patient;
}

protected List<Measure> getMeasures(
List<String> measureIds, List<String> measureIdentifiers, List<CanonicalType> measureCanonicals) {
boolean hasMeasureIds = measureIds != null && !measureIds.isEmpty();
boolean hasMeasureIdentifiers = measureIdentifiers != null && !measureIdentifiers.isEmpty();
boolean hasMeasureUrls = measureCanonicals != null && !measureCanonicals.isEmpty();
if (!hasMeasureIds && !hasMeasureIdentifiers && !hasMeasureUrls) {
return Collections.emptyList();
}

List<Measure> measureList = new ArrayList<>();

if (hasMeasureIds) {
for (String measureId : measureIds) {
Measure measureById = resolveById(new IdType("Measure", measureId));
measureList.add(measureById);
}
}

if (hasMeasureUrls) {
for (CanonicalType measureCanonical : measureCanonicals) {
Measure measureByUrl = resolveByUrl(measureCanonical);
measureList.add(measureByUrl);
}
}

// TODO: implement searching by measure identifiers
if (hasMeasureIdentifiers) {
throw new NotImplementedOperationException(
Msg.code(2278) + "Measure identifiers have not yet been implemented.");
}

Map<String, Measure> result = new HashMap<>();
measureList.forEach(measure -> result.putIfAbsent(measure.getUrl(), measure));

return new ArrayList<>(result.values());
}

protected Measure resolveByUrl(CanonicalType url) {
Canonicals.CanonicalParts parts = Canonicals.getParts(url);
Bundle result = this.repository.search(
Bundle.class, Measure.class, Searches.byNameAndVersion(parts.idPart(), parts.version()));
return (Measure) result.getEntryFirstRep().getResource();
}

protected Measure resolveById(IdType id) {
return this.repository.read(Measure.class, id);
}

protected <T extends Resource> T addConfiguredResource(Class<T> resourceClass, String id, String key) {
T resource;
// read resource from repository
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
import static org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils.getFullUrl;

import com.google.common.base.Strings;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Bundle.BundleType;
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.Endpoint;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Measure;
Expand All @@ -23,76 +22,134 @@
import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils;
import org.opencds.cqf.fhir.utility.Ids;
import org.opencds.cqf.fhir.utility.builder.BundleBuilder;
import org.opencds.cqf.fhir.utility.monad.Either3;
import org.opencds.cqf.fhir.utility.repository.Repositories;

// Alternate MeasureService call to Process MeasureEvaluation for the selected population of subjects against n-number
// of measure resources. The output of this operation would be a bundle of MeasureReports instead of MeasureReport.

public class R4MultiMeasureService {
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(R4MultiMeasureService.class);
private final Repository repository;
private Repository repository;
private final MeasureEvaluationOptions measureEvaluationOptions;
private String serverBase;

private final R4RepositorySubjectProvider subjectProvider;

private R4MeasureProcessor r4Processor;

private R4MeasureServiceUtils r4MeasureServiceUtils;

public R4MultiMeasureService(
Repository repository, MeasureEvaluationOptions measureEvaluationOptions, String serverBase) {
this.repository = repository;
this.measureEvaluationOptions = measureEvaluationOptions;
this.serverBase = serverBase;

subjectProvider = new R4RepositorySubjectProvider();

r4Processor = new R4MeasureProcessor(repository, this.measureEvaluationOptions, subjectProvider);

r4MeasureServiceUtils = new R4MeasureServiceUtils(repository);
}

public Bundle evaluate(
List<Either3<CanonicalType, IdType, Measure>> measures,
List<IdType> measureId,
List<String> measureUrl,
String periodStart,
String periodEnd,
String reportType,
String subjectId,
String subject, // practitioner passed in here
Endpoint contentEndpoint,
Endpoint terminologyEndpoint,
Endpoint dataEndpoint,
Bundle additionalData,
Parameters parameters,
String productLine,
String practitioner,
String reporter) {

var repo = Repositories.proxy(repository, true, dataEndpoint, contentEndpoint, terminologyEndpoint);

var subjectProvider = new R4RepositorySubjectProvider();
if (dataEndpoint != null && contentEndpoint != null && terminologyEndpoint != null) {
// if needing to use proxy repository, override constructors
repository = Repositories.proxy(repository, true, dataEndpoint, contentEndpoint, terminologyEndpoint);

var processor = new R4MeasureProcessor(repo, this.measureEvaluationOptions, subjectProvider);
r4Processor = new R4MeasureProcessor(repository, this.measureEvaluationOptions, subjectProvider);

R4MeasureServiceUtils r4MeasureServiceUtils = new R4MeasureServiceUtils(repository);
r4MeasureServiceUtils = new R4MeasureServiceUtils(repository);
}
r4MeasureServiceUtils.ensureSupplementalDataElementSearchParameter();

List<Measure> measures = r4MeasureServiceUtils.getMeasures(measureId, null, measureUrl);
log.info("multi-evaluate-measure, measures to evaluate: {}", measures.size());

var evalType = MeasureEvalType.fromCode(reportType)
.orElse(
subjectId == null || subjectId.isEmpty()
? MeasureEvalType.POPULATION
: MeasureEvalType.SUBJECT);
.orElse(subject == null || subject.isEmpty() ? MeasureEvalType.POPULATION : MeasureEvalType.SUBJECT);

// get subjects
var subjects = getSubjects(subjectProvider, practitioner, subjectId, evalType);
var subjects = getSubjects(subjectProvider, subject, evalType);

// create bundle
Bundle bundle = new BundleBuilder<>(Bundle.class)
.withType(BundleType.SEARCHSET.toString())
.build();

for (Either3<CanonicalType, IdType, Measure> measure : measures) {
// evaluate Measures
if (evalType.equals(MeasureEvalType.POPULATION) || evalType.equals(MeasureEvalType.SUBJECTLIST)) {
populationMeasureReport(
bundle,
measures,
periodStart,
periodEnd,
reportType,
evalType,
subject,
subjects,
parameters,
additionalData,
productLine,
reporter);
} else {
subjectMeasureReport(
bundle,
measures,
periodStart,
periodEnd,
reportType,
evalType,
subjects,
parameters,
additionalData,
productLine,
reporter);
}

return bundle;
}

protected void populationMeasureReport(
Bundle bundle,
List<Measure> measures,
String periodStart,
String periodEnd,
String reportType,
MeasureEvalType evalType,
String subjectParam,
List<String> subjects,
Parameters parameters,
Bundle additionalData,
String productLine,
String reporter) {

// one aggregated MeasureReport per Measure
var totalMeasures = measures.size();
for (Measure measure : measures) {
MeasureReport measureReport;
// evaluate each measure
measureReport = processor.evaluateMeasure(
measureReport = r4Processor.evaluateMeasure(
measure, periodStart, periodEnd, reportType, subjects, additionalData, parameters, evalType);

// add ProductLine after report is generated
measureReport = r4MeasureServiceUtils.addProductLineExtension(measureReport, productLine);

// add subject reference for non-individual reportTypes
measureReport = r4MeasureServiceUtils.addSubjectReference(measureReport, practitioner, subjectId);
measureReport = r4MeasureServiceUtils.addSubjectReference(measureReport, null, subjectParam);

// add reporter if available
if (reporter != null && !reporter.isEmpty()) {
Expand All @@ -107,25 +164,78 @@ public Bundle evaluate(
// progress feedback
var measureUrl = measureReport.getMeasure();
if (!measureUrl.isEmpty()) {
log.info("MeasureReport complete for Measure: {}", measureUrl);
log.debug(
"Completed evaluation for Measure: {}, Measures remaining to evaluate: {}",
measureUrl,
totalMeasures--);
}
}

return bundle;
}

protected List<String> getSubjects(
R4RepositorySubjectProvider subjectProvider,
String practitioner,
String subjectId,
MeasureEvalType evalType) {
// check for practitioner parameter before subject
if (StringUtils.isNotBlank(practitioner)) {
if (!practitioner.contains("/")) {
practitioner = "Practitioner/".concat(practitioner);
protected void subjectMeasureReport(
Bundle bundle,
List<Measure> measures,
String periodStart,
String periodEnd,
String reportType,
MeasureEvalType evalType,
List<String> subjects,
Parameters parameters,
Bundle additionalData,
String productLine,
String reporter) {

// create individual reports for each subject, and each measure
var totalReports = subjects.size() * measures.size();
var totalMeasures = measures.size();
log.debug(
"Evaluating individual MeasureReports for {} patients, and {} measures",
subjects.size(),
measures.size());
for (Measure measure : measures) {
for (String subject : subjects) {
MeasureReport measureReport;
// evaluate each measure
measureReport = r4Processor.evaluateMeasure(
measure,
periodStart,
periodEnd,
reportType,
Collections.singletonList(subject),
additionalData,
parameters,
evalType);

// add ProductLine after report is generated
measureReport = r4MeasureServiceUtils.addProductLineExtension(measureReport, productLine);

// add reporter if available
if (reporter != null && !reporter.isEmpty()) {
measureReport.setReporter(r4MeasureServiceUtils.getReporter(reporter));
}
// add id to measureReport
initializeReport(measureReport);

// add report to bundle
bundle.addEntry(getBundleEntry(serverBase, measureReport));

// progress feedback
var measureUrl = measureReport.getMeasure();
if (!measureUrl.isEmpty()) {
log.debug("MeasureReports remaining to evaluate {}", totalReports--);
}
}
if (measure.hasUrl()) {
log.info(
"Completed evaluation for Measure: {}, Measures remaining to evaluate: {}",
measure.getUrl(),
totalMeasures--);
}
subjectId = practitioner;
}
}

protected List<String> getSubjects(
R4RepositorySubjectProvider subjectProvider, String subjectId, MeasureEvalType evalType) {

return subjectProvider.getSubjects(repository, evalType, subjectId).collect(Collectors.toList());
}
Expand Down
Loading

0 comments on commit f196719

Please sign in to comment.