Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import java.util.List;
import org.hl7.fhir.exceptions.FHIRException;
Expand All @@ -20,6 +21,7 @@
import org.opencds.cqf.fhir.cr.hapi.common.StringTimePeriodHandler;
import org.opencds.cqf.fhir.cr.hapi.r4.R4MeasureEvaluatorMultipleFactory;
import org.opencds.cqf.fhir.cr.hapi.r4.R4MeasureEvaluatorSingleFactory;
import org.opencds.cqf.fhir.cr.measure.common.InvalidMeasureRequestException;
import org.opencds.cqf.fhir.cr.measure.common.MeasureEnvironment;
import org.opencds.cqf.fhir.cr.measure.common.MeasureReference;

Expand Down Expand Up @@ -90,18 +92,22 @@ public MeasureReport evaluateMeasure(
var dataEndpointParam = (Endpoint) getEndpoint(fhirVersion, dataEndpoint);
var environment = new MeasureEnvironment(
contentEndpointParam, terminologyEndpointParam, dataEndpointParam, additionalData);
return r4MeasureServiceFactory
.create(requestDetails, environment)
.evaluate(
new MeasureReference.ById(id),
stringTimePeriodHandler.getStartZonedDateTime(periodStart, requestDetails),
stringTimePeriodHandler.getEndZonedDateTime(periodEnd, requestDetails),
reportType,
subject,
lastReceivedOn,
parameters,
productLine,
practitioner);
try {
return r4MeasureServiceFactory
.create(requestDetails, environment)
.evaluate(
new MeasureReference.ById(id),
stringTimePeriodHandler.getStartZonedDateTime(periodStart, requestDetails),
stringTimePeriodHandler.getEndZonedDateTime(periodEnd, requestDetails),
reportType,
subject,
lastReceivedOn,
parameters,
productLine,
practitioner);
} catch (InvalidMeasureRequestException exception) {
throw new InvalidRequestException(exception.getMessage(), exception);
}
}

/**
Expand Down Expand Up @@ -158,16 +164,20 @@ public Parameters evaluate(
var measureRefs = MeasureReference.fromOperationParams(measureId, measureIdentifier, measureUrl);
var environment = new MeasureEnvironment(
contentEndpointParam, terminologyEndpointParam, dataEndpointParam, additionalData);
return r4MultiMeasureServiceFactory
.create(requestDetails, environment)
.evaluate(
measureRefs,
stringTimePeriodHandler.getStartZonedDateTime(periodStart, requestDetails),
stringTimePeriodHandler.getEndZonedDateTime(periodEnd, requestDetails),
reportType,
subject,
parameters,
productLine,
reporter);
try {
return r4MultiMeasureServiceFactory
.create(requestDetails, environment)
.evaluate(
measureRefs,
stringTimePeriodHandler.getStartZonedDateTime(periodStart, requestDetails),
stringTimePeriodHandler.getEndZonedDateTime(periodEnd, requestDetails),
reportType,
subject,
parameters,
productLine,
reporter);
} catch (InvalidMeasureRequestException exception) {
throw new InvalidRequestException(exception.getMessage(), exception);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* stratifier with no {@code criteria.expression} and no components, or other shape errors that make
* the Measure un-evaluable.
*/
public class InvalidMeasureDefinitionException extends RuntimeException {
public class InvalidMeasureDefinitionException extends InvalidMeasureRequestException {
public InvalidMeasureDefinitionException(String message) {
super(message);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.opencds.cqf.fhir.cr.measure.common;

/**
* Base class for measure evaluation errors caused by invalid client input — the request itself is
* malformed or refers to resources that cannot be resolved. Subclasses are translated to {@code
* ca.uhn.fhir.rest.server.exceptions.InvalidRequestException} (HTTP 400) at the HAPI provider
* boundary so that a single {@code catch} can cover the whole family.
*
* <p>Service-layer code in {@code cqf-fhir-cr} should throw a subclass of this type rather than a
* HAPI exception directly, keeping the service layer independent of HAPI's HTTP bindings.
*/
public class InvalidMeasureRequestException extends RuntimeException {
public InvalidMeasureRequestException(String message) {
super(message);
}

public InvalidMeasureRequestException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.opencds.cqf.fhir.cr.measure.common;

/**
* Thrown when the request cannot be resolved to a unique Measure to evaluate — e.g. the measure
* URL matches no resources, matches multiple resources, or no measure reference was supplied at
* all.
*/
public class MeasureLookupException extends InvalidMeasureRequestException {
public MeasureLookupException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.opencds.cqf.fhir.cr.measure.common.CompositeEvaluationResultsPerMeasure;
import org.opencds.cqf.fhir.cr.measure.common.MeasureDef;
import org.opencds.cqf.fhir.cr.measure.common.MeasureEvalType;
import org.opencds.cqf.fhir.cr.measure.common.MeasureLookupException;
import org.opencds.cqf.fhir.cr.measure.common.MeasurePeriodValidator;
import org.opencds.cqf.fhir.cr.measure.common.MeasureReference;
import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils;
Expand Down Expand Up @@ -228,6 +229,11 @@ private List<List<MeasureDefAndR4MeasureReport>> evaluateToListOfList(
String reporter,
@Nullable String practitioner) {

if (measureRefs == null || measureRefs.isEmpty()) {
throw new MeasureLookupException(
"At least one of measureId, measureIdentifier, or measureUrl must be supplied");
}

measurePeriodValidator.validatePeriodStartAndEnd(periodStart, periodEnd);

var r4ProcessorToUse = new R4MeasureProcessor(resolvedRepo, this.measureEvaluationOptions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import org.hl7.fhir.r4.model.SearchParameter;
import org.hl7.fhir.r4.model.StringType;
import org.opencds.cqf.fhir.cr.measure.common.MeasureEvalType;
import org.opencds.cqf.fhir.cr.measure.common.MeasureLookupException;
import org.opencds.cqf.fhir.cr.measure.common.MeasureReference;
import org.opencds.cqf.fhir.cr.measure.common.MeasureReportType;
import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring;
Expand Down Expand Up @@ -203,6 +204,14 @@ public Measure resolveByUrl(String url) {
}

Bundle result = this.repository.search(Bundle.class, Measure.class, searchParameters);

if (result == null || result.getEntry().isEmpty()) {
throw new MeasureLookupException("Measure URL: %s, found no matching measure resources".formatted(url));
}
if (result.getEntry().size() > 1) {
throw new MeasureLookupException(
"Measure URL: %s, found more than one matching measure resource".formatted(url));
}
return (Measure) result.getEntryFirstRep().getResource();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.opencds.cqf.fhir.cr.measure.r4;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand All @@ -10,6 +11,7 @@
import java.util.Date;
import org.hl7.fhir.r4.model.MeasureReport.MeasureReportStatus;
import org.junit.jupiter.api.Test;
import org.opencds.cqf.fhir.cr.measure.common.MeasureLookupException;
import org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType;
import org.opencds.cqf.fhir.cr.measure.r4.MultiMeasure.Given;

Expand Down Expand Up @@ -1103,4 +1105,17 @@ void MultiMeasure_ThrowsErrorWithDuplicatePopulationIds() {
assertTrue(e.getMessage().contains("Duplicate population ID"));
assertTrue(e.getMessage().contains("initial-population"));
}

@Test
void MultiMeasure_NoMeasureReferencesSupplied_throws() {
var when = GIVEN_REPO
.when()
.periodStart("2024-01-01")
.periodEnd("2024-12-31")
.reportType("population")
.evaluate();

var e = assertThrows(MeasureLookupException.class, when::then);
assertEquals("At least one of measureId, measureIdentifier, or measureUrl must be supplied", e.getMessage());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,35 @@
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;

import ca.uhn.fhir.repository.IRepository;
import jakarta.annotation.Nullable;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.Measure;
import org.hl7.fhir.r4.model.MeasureReport;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.opencds.cqf.fhir.cr.measure.common.MeasureEvalType;
import org.opencds.cqf.fhir.cr.measure.common.MeasureLookupException;
import org.opencds.cqf.fhir.cr.measure.constant.MeasureReportConstants;
import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureEvalType;
import org.opencds.cqf.fhir.utility.monad.Either;
Expand Down Expand Up @@ -266,4 +275,42 @@
private static Either<Optional<Reference>, Exception> buildEitherLeft(@Nullable String theId) {
return Eithers.forLeft(Optional.ofNullable(theId).map(Reference::new));
}

@Test
void resolveByUrl_noMatchingMeasures_throws() {
var url = "http://example.org/Measure/does-not-exist";
when(repository.search(eq(Bundle.class), eq(Measure.class), any(Map.class)))

Check warning on line 282 in cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureServiceUtilsTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this use of "search"; it is deprecated.

See more on https://sonarcloud.io/project/issues?id=cqframework_clinical-reasoning&issues=AZ30bv7uTpoB5hAMAICr&open=AZ30bv7uTpoB5hAMAICr&pullRequest=1025
.thenReturn(new Bundle());

var actual = assertThrows(MeasureLookupException.class, () -> testSubject.resolveByUrl(url));

assertEquals("Measure URL: %s, found no matching measure resources".formatted(url), actual.getMessage());
}

@Test
void resolveByUrl_multipleMatches_throws() {
var url = "http://example.org/Measure/ambiguous";
var bundle = new Bundle();
bundle.addEntry().setResource(new Measure().setUrl(url));
bundle.addEntry().setResource(new Measure().setUrl(url));
when(repository.search(eq(Bundle.class), eq(Measure.class), any(Map.class)))

Check warning on line 296 in cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureServiceUtilsTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this use of "search"; it is deprecated.

See more on https://sonarcloud.io/project/issues?id=cqframework_clinical-reasoning&issues=AZ30bv7uTpoB5hAMAICs&open=AZ30bv7uTpoB5hAMAICs&pullRequest=1025
.thenReturn(bundle);

var actual = assertThrows(MeasureLookupException.class, () -> testSubject.resolveByUrl(url));

assertEquals(
"Measure URL: %s, found more than one matching measure resource".formatted(url), actual.getMessage());
}

@Test
void resolveByUrl_singleMatch_returns() {
var url = "http://example.org/Measure/found";
var measure = new Measure().setUrl(url);
var bundle = new Bundle();
bundle.addEntry().setResource(measure);
when(repository.search(eq(Bundle.class), eq(Measure.class), any(Map.class)))

Check warning on line 311 in cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureServiceUtilsTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this use of "search"; it is deprecated.

See more on https://sonarcloud.io/project/issues?id=cqframework_clinical-reasoning&issues=AZ30bv7uTpoB5hAMAICt&open=AZ30bv7uTpoB5hAMAICt&pullRequest=1025
.thenReturn(bundle);

assertSame(measure, testSubject.resolveByUrl(url));
}
}
Loading