Skip to content

Commit

Permalink
multi-measure service capability (#453)
Browse files Browse the repository at this point in the history
* multi-measure service, testing, and shared service utils

* fix subject reference on MR

* spotless edit

* date rolled expired test case

* additional coverage for measureService

* Fix up some warnings, remove some dead code

---------

Co-authored-by: Jonathan Percival <[email protected]>
  • Loading branch information
Capt-Mac and JPercival committed May 2, 2024
1 parent 0a6bea4 commit 52e3656
Show file tree
Hide file tree
Showing 33 changed files with 1,650 additions and 241 deletions.
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"DENOMINATOREXCEPTION",
"DENOMINATOREXCLUSION",
"DEPENDSON",
"deqm",
"DERIVEDFROM",
"Dischargedon",
"dstu",
Expand Down Expand Up @@ -65,17 +66,20 @@
"questionnaireresponse",
"RESOURCETYPE",
"sdes",
"SEARCHPARAMETER",
"SEARCHSET",
"sgpc",
"Sonarlint",
"Stratifier",
"stratifiers",
"SUBJECTLIST",
"SUPPLEMENTALDATA",
"testng",
"unsignedint",
"Unvalidated",
"uscore",
"VALUESET",
"valuesets",
"Versionless"
],
"cSpell.enabledLanguageIds": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ public void setupTrial() throws Exception {

Bundle additionalData = (Bundle) FhirContext.forR4Cached()
.newJsonParser()
.parseResource(
this.getClass().getResourceAsStream("CaseRepresentation101/generated.json"));
.parseResource(this.getClass().getResourceAsStream("CaseRepresentation101/generated.json"));

this.when = Measure.given()
.repositoryFor("CaseRepresentation101")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import static org.opencds.cqf.fhir.cr.measure.constant.MeasureReportConstants.MEASUREREPORT_IMPROVEMENT_NOTATION_SYSTEM;
import static org.opencds.cqf.fhir.cr.measure.constant.MeasureReportConstants.MEASUREREPORT_MEASURE_POPULATION_SYSTEM;
import static org.opencds.cqf.fhir.cr.measure.constant.MeasureReportConstants.MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION;
import static org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils.getFullUrl;
import static org.opencds.cqf.fhir.utility.Resources.newResource;

import ca.uhn.fhir.context.FhirContext;
Expand Down Expand Up @@ -591,17 +592,6 @@ protected Parameters initializeResult() {
return newResource(Parameters.class, "care-gaps-report-" + UUID.randomUUID());
}

protected static String getFullUrl(String serverAddress, IBaseResource resource) {
checkArgument(
resource.getIdElement().hasIdPart(),
"Cannot generate a fullUrl because the resource does not have an id.");
return getFullUrl(serverAddress, resource.fhirType(), Ids.simplePart(resource));
}

protected static String getFullUrl(String serverAddress, String fhirType, String elementId) {
return String.format("%s%s/%s", serverAddress + (serverAddress.endsWith("/") ? "" : "/"), fhirType, elementId);
}

protected void checkValidStatusCode(List<String> statuses) {
for (String status : statuses) {
if (!status.equals(CareGapsStatusCode.CLOSED_GAP.toString())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,43 +1,21 @@
package org.opencds.cqf.fhir.cr.measure.r4;

import static org.opencds.cqf.fhir.cr.measure.constant.MeasureReportConstants.COUNTRY_CODING_SYSTEM_CODE;
import static org.opencds.cqf.fhir.cr.measure.constant.MeasureReportConstants.MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION;
import static org.opencds.cqf.fhir.cr.measure.constant.MeasureReportConstants.MEASUREREPORT_PRODUCT_LINE_EXT_URL;
import static org.opencds.cqf.fhir.cr.measure.constant.MeasureReportConstants.MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_DEFINITION_DATE;
import static org.opencds.cqf.fhir.cr.measure.constant.MeasureReportConstants.MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_URL;
import static org.opencds.cqf.fhir.cr.measure.constant.MeasureReportConstants.MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_VERSION;
import static org.opencds.cqf.fhir.cr.measure.constant.MeasureReportConstants.US_COUNTRY_CODE;
import static org.opencds.cqf.fhir.cr.measure.constant.MeasureReportConstants.US_COUNTRY_DISPLAY;

import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException;
import ca.uhn.fhir.util.BundleBuilder;
import java.util.Collections;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.ContactDetail;
import org.hl7.fhir.r4.model.ContactPoint;
import org.hl7.fhir.r4.model.Endpoint;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Measure;
import org.hl7.fhir.r4.model.MeasureReport;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.SearchParameter;
import org.hl7.fhir.r4.model.StringType;
import org.opencds.cqf.fhir.api.Repository;
import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions;
import org.opencds.cqf.fhir.cr.measure.common.MeasureReportType;
import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils;
import org.opencds.cqf.fhir.utility.monad.Either3;
import org.opencds.cqf.fhir.utility.repository.Repositories;

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

Expand All @@ -64,12 +42,13 @@ public MeasureReport evaluate(
var repo = Repositories.proxy(repository, true, dataEndpoint, contentEndpoint, terminologyEndpoint);
var processor = new R4MeasureProcessor(repo, this.measureEvaluationOptions, new R4RepositorySubjectProvider());

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

MeasureReport measureReport = null;

if (StringUtils.isNotBlank(practitioner)) {
if (practitioner.indexOf("/") == -1) {
if (!practitioner.contains("/")) {
practitioner = "Practitioner/".concat(practitioner);
}
subjectId = practitioner;
Expand All @@ -85,73 +64,9 @@ public MeasureReport evaluate(
parameters);

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

// add subject reference for non-individual reportTypes
if ((StringUtils.isNotBlank(practitioner) || StringUtils.isNotBlank(subjectId))
&& (measureReport.getType().name().equals(MeasureReportType.SUMMARY.name())
|| measureReport.getType().name().equals(MeasureReportType.SUBJECTLIST.name()))) {
if (StringUtils.isNotBlank(practitioner)) {
measureReport.setSubject(new Reference(practitioner));
} else {
measureReport.setSubject(new Reference(subjectId));
}
}
return measureReport;
return r4MeasureServiceUtils.addSubjectReference(measureReport, practitioner, subjectId);
}

private void addProductLineExtension(MeasureReport measureReport, String productLine) {
if (productLine != null) {
Extension ext = new Extension();
ext.setUrl(MEASUREREPORT_PRODUCT_LINE_EXT_URL);
ext.setValue(new StringType(productLine));
measureReport.addExtension(ext);
}
}

protected void ensureSupplementalDataElementSearchParameter() {
// create a transaction bundle
BundleBuilder builder = new BundleBuilder(repository.fhirContext());

// set the request to be condition on code == supplemental data
builder.addTransactionCreateEntry(SUPPLEMENTAL_DATA_SEARCHPARAMETER).conditional("code=supplemental-data");
try {
repository.transaction(builder.getBundle());
} catch (NotImplementedOperationException e) {
log.warn(
"Error creating supplemental data search parameter. This may be due to the server not supporting transactions.",
e);
}
}

public static final List<ContactDetail> CQI_CONTACTDETAIL = Collections.singletonList(new ContactDetail()
.addTelecom(new ContactPoint()
.setSystem(ContactPoint.ContactPointSystem.URL)
.setValue("http:https://www.hl7.org/Special/committees/cqi/index.cfm")));

public static final List<CodeableConcept> US_JURISDICTION_CODING = Collections.singletonList(new CodeableConcept()
.addCoding(new Coding(COUNTRY_CODING_SYSTEM_CODE, US_COUNTRY_CODE, US_COUNTRY_DISPLAY)));

public static final SearchParameter SUPPLEMENTAL_DATA_SEARCHPARAMETER = (SearchParameter) new SearchParameter()
.setUrl(MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_URL)
.setVersion(MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_VERSION)
.setName("DEQMMeasureReportSupplementalData")
.setStatus(Enumerations.PublicationStatus.ACTIVE)
.setDate(MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_DEFINITION_DATE)
.setPublisher("HL7 International - Clinical Quality Information Work Group")
.setContact(CQI_CONTACTDETAIL)
.setDescription(String.format(
"Returns resources (supplemental data) from references on extensions on the MeasureReport with urls matching %s.",
MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION))
.setJurisdiction(US_JURISDICTION_CODING)
.addBase("MeasureReport")
.setCode("supplemental-data")
.setType(Enumerations.SearchParamType.REFERENCE)
.setExpression(String.format(
"MeasureReport.extension('%s').value", MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION))
.setXpath(String.format(
"f:MeasureReport/f:extension[@url='%s'].value", MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION))
.setXpathUsage(SearchParameter.XPathUsageType.NORMAL)
.setTitle("Supplemental Data")
.setId("deqm-measurereport-supplemental-data");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package org.opencds.cqf.fhir.cr.measure.r4;

import static org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils.getFullUrl;

import com.google.common.base.Strings;
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;
import org.hl7.fhir.r4.model.MeasureReport;
import org.hl7.fhir.r4.model.Parameters;
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.MeasureEvalType;
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 final MeasureEvaluationOptions measureEvaluationOptions;
private final CareGapsProperties careGapsProperties;

public R4MultiMeasureService(
Repository repository,
MeasureEvaluationOptions measureEvaluationOptions,
CareGapsProperties careGapsProperties) {
this.repository = repository;
this.measureEvaluationOptions = measureEvaluationOptions;
this.careGapsProperties = careGapsProperties;
}

public Bundle evaluate(
List<Either3<CanonicalType, IdType, Measure>> measures,
String periodStart,
String periodEnd,
String reportType,
String subjectId,
Endpoint contentEndpoint,
Endpoint terminologyEndpoint,
Endpoint dataEndpoint,
Bundle additionalData,
Parameters parameters,
String productLine,
String practitioner) {

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

var subjectProvider = new R4RepositorySubjectProvider();

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

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

log.info("multi-evaluate-measure, measures to evaluate: {}", measures.size());

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

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

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

for (Either3<CanonicalType, IdType, Measure> measure : measures) {
MeasureReport measureReport;
// evaluate each measure
measureReport = processor.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);
// add id to measureReport
initializeReport(measureReport);

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

// progress feedback
var measureUrl = measureReport.getMeasure();
if (!measureUrl.isEmpty()) {
log.info("MeasureReport complete for Measure: {}", measureUrl);
}
}

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);
}
subjectId = practitioner;
}

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

protected void initializeReport(MeasureReport measureReport) {
if (Strings.isNullOrEmpty(measureReport.getId())) {
IIdType id = Ids.newId(MeasureReport.class, UUID.randomUUID().toString());
measureReport.setId(id);
}
}

protected Bundle.BundleEntryComponent getBundleEntry(String serverBase, Resource resource) {
return new Bundle.BundleEntryComponent().setResource(resource).setFullUrl(getFullUrl(serverBase, resource));
}
}
Loading

0 comments on commit 52e3656

Please sign in to comment.