diff --git a/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/measure/r4/Measure.java b/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/measure/r4/Measure.java index a79d0446fa..e611583443 100644 --- a/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/measure/r4/Measure.java +++ b/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/measure/r4/Measure.java @@ -149,10 +149,6 @@ public When evaluate() { null, null, null, - null, - additionalData, - null, - null, null); return this; } diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/dstu3/CrDstu3Config.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/dstu3/CrDstu3Config.java index 2363be9dcd..b0f736837b 100644 --- a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/dstu3/CrDstu3Config.java +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/dstu3/CrDstu3Config.java @@ -26,7 +26,8 @@ public class CrDstu3Config { @Bean IMeasureServiceFactory dstu3MeasureServiceFactory( IRepositoryFactory repositoryFactory, MeasureEvaluationOptions evaluationOptions) { - return rd -> new Dstu3MeasureService(repositoryFactory.create(rd), evaluationOptions); + return (requestDetails, environment) -> new Dstu3MeasureService( + environment.resolve(repositoryFactory.create(requestDetails)), evaluationOptions); } @Bean diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/CrR4Config.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/CrR4Config.java index 0695e47b6f..2cc201c943 100644 --- a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/CrR4Config.java +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/CrR4Config.java @@ -75,8 +75,8 @@ R4MeasureEvaluatorSingleFactory r4MeasureServiceFactory( MeasureEvaluationOptions evaluationOptions, MeasurePeriodValidator measurePeriodValidator) { // We are effectively returning an R4MeasureEvaluatorSingle her - return requestDetails -> new R4MultiMeasureService( - repositoryFactory.create(requestDetails), + return (requestDetails, environment) -> new R4MultiMeasureService( + environment.resolve(repositoryFactory.create(requestDetails)), evaluationOptions, requestDetails.getFhirServerBase(), measurePeriodValidator); @@ -87,8 +87,11 @@ R4MeasureEvaluatorMultipleFactory r4MeasureEvaluatorMultipleFactory( IRepositoryFactory repositoryFactory, MeasureEvaluationOptions evaluationOptions, MeasurePeriodValidator measurePeriodValidator) { - return rd -> new R4MultiMeasureService( - repositoryFactory.create(rd), evaluationOptions, rd.getFhirServerBase(), measurePeriodValidator); + return (requestDetails, environment) -> new R4MultiMeasureService( + environment.resolve(repositoryFactory.create(requestDetails)), + evaluationOptions, + requestDetails.getFhirServerBase(), + measurePeriodValidator); } @Bean diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/dstu3/IMeasureServiceFactory.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/dstu3/IMeasureServiceFactory.java index 538a07ea2e..f754fe4160 100644 --- a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/dstu3/IMeasureServiceFactory.java +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/dstu3/IMeasureServiceFactory.java @@ -1,9 +1,10 @@ package org.opencds.cqf.fhir.cr.hapi.dstu3; import ca.uhn.fhir.rest.api.server.RequestDetails; +import org.opencds.cqf.fhir.cr.measure.common.MeasureEnvironment; import org.opencds.cqf.fhir.cr.measure.dstu3.Dstu3MeasureService; @FunctionalInterface public interface IMeasureServiceFactory { - Dstu3MeasureService create(RequestDetails requestDetails); + Dstu3MeasureService create(RequestDetails requestDetails, MeasureEnvironment environment); } diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/dstu3/measure/MeasureOperationsProvider.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/dstu3/measure/MeasureOperationsProvider.java index a6db93e2fd..6fdc6c4aa2 100644 --- a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/dstu3/measure/MeasureOperationsProvider.java +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/dstu3/measure/MeasureOperationsProvider.java @@ -18,6 +18,7 @@ import org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent; import org.hl7.fhir.exceptions.FHIRException; import org.opencds.cqf.fhir.cr.hapi.dstu3.IMeasureServiceFactory; +import org.opencds.cqf.fhir.cr.measure.common.MeasureEnvironment; import org.springframework.stereotype.Component; @Component @@ -72,8 +73,9 @@ public MeasureReport evaluateMeasure( RequestDetails requestDetails) throws InternalErrorException, FHIRException { var terminologyEndpointParam = (Endpoint) getEndpoint(fhirVersion, terminologyEndpoint); + var environment = new MeasureEnvironment(null, terminologyEndpointParam, null, additionalData); return dstu3MeasureProcessorFactory - .create(requestDetails) + .create(requestDetails, environment) .evaluateMeasure( id, periodStart, @@ -83,8 +85,6 @@ public MeasureReport evaluateMeasure( practitioner, lastReceivedOn, productLine, - additionalData, - parameters, - terminologyEndpointParam); + parameters); } } diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/R4MeasureEvaluatorMultipleFactory.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/R4MeasureEvaluatorMultipleFactory.java index 5e4434dc6b..77f32e4cc9 100644 --- a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/R4MeasureEvaluatorMultipleFactory.java +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/R4MeasureEvaluatorMultipleFactory.java @@ -1,9 +1,10 @@ package org.opencds.cqf.fhir.cr.hapi.r4; import ca.uhn.fhir.rest.api.server.RequestDetails; +import org.opencds.cqf.fhir.cr.measure.common.MeasureEnvironment; import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureEvaluatorMultiple; @FunctionalInterface public interface R4MeasureEvaluatorMultipleFactory { - R4MeasureEvaluatorMultiple create(RequestDetails requestDetails); + R4MeasureEvaluatorMultiple create(RequestDetails requestDetails, MeasureEnvironment environment); } diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/R4MeasureEvaluatorSingleFactory.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/R4MeasureEvaluatorSingleFactory.java index a2279dc7dd..d213110b58 100644 --- a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/R4MeasureEvaluatorSingleFactory.java +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/R4MeasureEvaluatorSingleFactory.java @@ -1,9 +1,10 @@ package org.opencds.cqf.fhir.cr.hapi.r4; import ca.uhn.fhir.rest.api.server.RequestDetails; +import org.opencds.cqf.fhir.cr.measure.common.MeasureEnvironment; import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureEvaluatorSingle; @FunctionalInterface public interface R4MeasureEvaluatorSingleFactory { - R4MeasureEvaluatorSingle create(RequestDetails requestDetails); + R4MeasureEvaluatorSingle create(RequestDetails requestDetails, MeasureEnvironment environment); } diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/measure/MeasureOperationsProvider.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/measure/MeasureOperationsProvider.java index 342d696ef9..3e1012f889 100644 --- a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/measure/MeasureOperationsProvider.java +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/measure/MeasureOperationsProvider.java @@ -20,6 +20,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.MeasureEnvironment; import org.opencds.cqf.fhir.cr.measure.common.MeasureReference; @SuppressWarnings("java:S107") @@ -87,8 +88,10 @@ public MeasureReport evaluateMeasure( var contentEndpointParam = (Endpoint) getEndpoint(fhirVersion, contentEndpoint); var terminologyEndpointParam = (Endpoint) getEndpoint(fhirVersion, terminologyEndpoint); var dataEndpointParam = (Endpoint) getEndpoint(fhirVersion, dataEndpoint); + var environment = new MeasureEnvironment( + contentEndpointParam, terminologyEndpointParam, dataEndpointParam, additionalData); return r4MeasureServiceFactory - .create(requestDetails) + .create(requestDetails, environment) .evaluate( new MeasureReference.ById(id), stringTimePeriodHandler.getStartZonedDateTime(periodStart, requestDetails), @@ -96,10 +99,6 @@ public MeasureReport evaluateMeasure( reportType, subject, lastReceivedOn, - contentEndpointParam, - terminologyEndpointParam, - dataEndpointParam, - additionalData, parameters, productLine, practitioner); @@ -157,18 +156,16 @@ public Parameters evaluate( var terminologyEndpointParam = (Endpoint) getEndpoint(fhirVersion, terminologyEndpoint); var dataEndpointParam = (Endpoint) getEndpoint(fhirVersion, dataEndpoint); var measureRefs = MeasureReference.fromOperationParams(measureId, measureIdentifier, measureUrl); + var environment = new MeasureEnvironment( + contentEndpointParam, terminologyEndpointParam, dataEndpointParam, additionalData); return r4MultiMeasureServiceFactory - .create(requestDetails) + .create(requestDetails, environment) .evaluate( measureRefs, stringTimePeriodHandler.getStartZonedDateTime(periodStart, requestDetails), stringTimePeriodHandler.getEndZonedDateTime(periodEnd, requestDetails), reportType, subject, - contentEndpointParam, - terminologyEndpointParam, - dataEndpointParam, - additionalData, parameters, productLine, reporter); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEnvironment.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEnvironment.java new file mode 100644 index 0000000000..88e0a8fff7 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEnvironment.java @@ -0,0 +1,58 @@ +package org.opencds.cqf.fhir.cr.measure.common; + +import ca.uhn.fhir.repository.IRepository; +import jakarta.annotation.Nullable; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.opencds.cqf.fhir.utility.repository.FederatedRepository; +import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository; +import org.opencds.cqf.fhir.utility.repository.Repositories; + +/** + * Version-agnostic environment configuration for measure evaluation. + * + *

Separates the infrastructure inputs (where data, content, and terminology come from) + * from the operation parameters (what to evaluate). Per the pipeline architecture, + * environment resolution happens before domain logic: the service layer composes a + * {@code ProxyRepository} from these endpoints and passes it to the evaluator. + * + *

Endpoint resources are typed as {@link IBaseResource} so both R4 and DSTU3 + * {@code Endpoint} instances can be carried without version coupling. + * {@code Repositories.proxy()} already accepts {@code IBaseResource} endpoints directly. + * + * @param contentEndpoint endpoint for library/content resolution (nullable) + * @param terminologyEndpoint endpoint for terminology resolution (nullable) + * @param dataEndpoint endpoint for clinical data retrieval (nullable) + * @param additionalData supplemental data bundle for repository federation and CQL engine + * configuration (nullable) + */ +public record MeasureEnvironment( + @Nullable IBaseResource contentEndpoint, + @Nullable IBaseResource terminologyEndpoint, + @Nullable IBaseResource dataEndpoint, + @Nullable IBaseBundle additionalData) { + + /** Empty environment — no endpoints, no additional data. */ + public static final MeasureEnvironment EMPTY = new MeasureEnvironment(null, null, null, null); + + /** + * Resolves this environment against a base repository. + * + *

If any endpoint is present, wraps {@code base} in a {@code ProxyRepository}; null + * endpoints fall back to {@code base}. If {@code additionalData} is present, federates + * the result with an in-memory repository seeded from that bundle. + * + * @param base the base repository to build on top of + * @return the resolved repository, possibly wrapped + */ + public IRepository resolve(IRepository base) { + IRepository repo = base; + if (dataEndpoint() != null || contentEndpoint() != null || terminologyEndpoint() != null) { + repo = Repositories.proxy(repo, true, dataEndpoint(), contentEndpoint(), terminologyEndpoint()); + } + if (additionalData() != null) { + repo = new FederatedRepository(repo, new InMemoryFhirRepository(repo.fhirContext(), additionalData())); + } + return repo; + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureEvaluatorSingle.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureEvaluatorSingle.java index 6c1a632164..9900210ec7 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureEvaluatorSingle.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureEvaluatorSingle.java @@ -1,7 +1,5 @@ package org.opencds.cqf.fhir.cr.measure.dstu3; -import org.hl7.fhir.dstu3.model.Bundle; -import org.hl7.fhir.dstu3.model.Endpoint; import org.hl7.fhir.dstu3.model.IdType; import org.hl7.fhir.dstu3.model.MeasureReport; import org.hl7.fhir.dstu3.model.Parameters; @@ -21,7 +19,5 @@ MeasureReport evaluateMeasure( String practitioner, String lastReceivedOn, String productLine, - Bundle additionalData, - Parameters parameters, - Endpoint terminologyEndpoint); + Parameters parameters); } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureService.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureService.java index 285b84f71a..2b23493a41 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureService.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureService.java @@ -12,12 +12,10 @@ import ca.uhn.fhir.util.BundleBuilder; import java.util.Collections; import java.util.List; -import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.dstu3.model.CodeableConcept; import org.hl7.fhir.dstu3.model.Coding; import org.hl7.fhir.dstu3.model.ContactDetail; import org.hl7.fhir.dstu3.model.ContactPoint; -import org.hl7.fhir.dstu3.model.Endpoint; import org.hl7.fhir.dstu3.model.Enumerations; import org.hl7.fhir.dstu3.model.Extension; import org.hl7.fhir.dstu3.model.IdType; @@ -89,8 +87,7 @@ public Dstu3MeasureService(IRepository repository, MeasureEvaluationOptions meas * received. * @param productLine the productLine (e.g. Medicare, Medicaid, etc) to use * for the evaluation. This is a non-standard parameter. - * @param additionalData the data bundle containing additional data - * @param terminologyEndpoint the endpoint of terminology services for your measure valuesets + * @param environment endpoint and supplemental data configuration * @return the calculated MeasureReport */ @Override @@ -103,16 +100,14 @@ public MeasureReport evaluateMeasure( String practitioner, String lastReceivedOn, String productLine, - Bundle additionalData, - Parameters parameters, - Endpoint terminologyEndpoint) { + Parameters parameters) { ensureSupplementalDataElementSearchParameter(); var dstu3MeasureProcessor = new Dstu3MeasureProcessor(repository, measureEvaluationOptions); MeasureReport report = dstu3MeasureProcessor.evaluateMeasure( - id, periodStart, periodEnd, reportType, Collections.singletonList(subject), additionalData, parameters); + id, periodStart, periodEnd, reportType, Collections.singletonList(subject), null, parameters); if (productLine != null) { Extension ext = new Extension(); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapsBundleBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapsBundleBuilder.java index c8aad3a974..8398a969ff 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapsBundleBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapsBundleBuilder.java @@ -113,10 +113,6 @@ public List makePatientBundles( subject, null, null, - null, - null, - null, - null, reporter); var entries = result.getParameter().stream() diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluatorMultiple.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluatorMultiple.java index 4c2d1ec083..d78ebab926 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluatorMultiple.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluatorMultiple.java @@ -3,8 +3,6 @@ import jakarta.annotation.Nullable; import java.time.ZonedDateTime; import java.util.List; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.Endpoint; import org.hl7.fhir.r4.model.Parameters; import org.opencds.cqf.fhir.cr.measure.common.MeasureReference; @@ -20,10 +18,6 @@ Parameters evaluate( @Nullable ZonedDateTime periodEnd, String reportType, String subject, // practitioner passed in here - Endpoint contentEndpoint, - Endpoint terminologyEndpoint, - Endpoint dataEndpoint, - Bundle additionalData, Parameters parameters, String productLine, String reporter); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluatorSingle.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluatorSingle.java index 83ecd01e83..fa8626ffa8 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluatorSingle.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluatorSingle.java @@ -2,8 +2,6 @@ import jakarta.annotation.Nullable; import java.time.ZonedDateTime; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.Endpoint; import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.Parameters; import org.opencds.cqf.fhir.cr.measure.common.MeasureReference; @@ -20,10 +18,6 @@ MeasureReport evaluate( String reportType, String subjectId, String lastReceivedOn, - Endpoint contentEndpoint, - Endpoint terminologyEndpoint, - Endpoint dataEndpoint, - Bundle additionalData, Parameters parameters, String productLine, String practitioner); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MultiMeasureService.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MultiMeasureService.java index 8650c5f10e..2636221dfc 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MultiMeasureService.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MultiMeasureService.java @@ -20,7 +20,6 @@ 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.Endpoint; import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.Parameters; @@ -36,9 +35,6 @@ 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.repository.FederatedRepository; -import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository; -import org.opencds.cqf.fhir.utility.repository.Repositories; /** * Alternate MeasureService call to Process MeasureEvaluation for the selected population of subjects against n-number @@ -54,8 +50,6 @@ public class R4MultiMeasureService implements R4MeasureEvaluatorSingle, R4Measur private final MeasurePeriodValidator measurePeriodValidator; private final String serverBase; private final R4RepositorySubjectProvider subjectProvider; - private final R4MeasureProcessor r4MeasureProcessorStandardRepository; - private final R4MeasureServiceUtils r4MeasureServiceUtilsStandardRepository; private enum SingleOrMultiple { SINGLE, @@ -72,8 +66,6 @@ public R4MultiMeasureService( this.measurePeriodValidator = measurePeriodValidator; this.serverBase = serverBase; this.subjectProvider = new R4RepositorySubjectProvider(measureEvaluationOptions.getSubjectProviderOptions()); - this.r4MeasureProcessorStandardRepository = new R4MeasureProcessor(repository, this.measureEvaluationOptions); - this.r4MeasureServiceUtilsStandardRepository = new R4MeasureServiceUtils(repository); } // We should eliminate this if/when we eliminate the Measure test class @@ -89,10 +81,6 @@ public MeasureReport evaluate( String reportType, String subjectId, String lastReceivedOn, - Endpoint contentEndpoint, - Endpoint terminologyEndpoint, - Endpoint dataEndpoint, - Bundle additionalData, Parameters parameters, String productLine, String practitioner) { @@ -104,10 +92,7 @@ public MeasureReport evaluate( reportType, subjectId, lastReceivedOn, - contentEndpoint, - terminologyEndpoint, - dataEndpoint, - additionalData, + repository, parameters, productLine, null) // reporter is null in the single measure case @@ -122,10 +107,7 @@ MeasureDefAndR4MeasureReport evaluateSingleMeasureCaptureDef( String reportType, String subjectId, String lastReceivedOn, - Endpoint contentEndpoint, - Endpoint terminologyEndpoint, - Endpoint dataEndpoint, - Bundle additionalData, + IRepository resolvedRepo, Parameters parameters, String productLine, String practitioner) { @@ -137,10 +119,7 @@ MeasureDefAndR4MeasureReport evaluateSingleMeasureCaptureDef( periodEnd, reportType, subjectId, - contentEndpoint, - terminologyEndpoint, - dataEndpoint, - additionalData, + resolvedRepo, parameters, productLine, null, @@ -170,10 +149,6 @@ public Parameters evaluate( @Nullable ZonedDateTime periodEnd, String reportType, String subject, // practitioner passed in here - Endpoint contentEndpoint, - Endpoint terminologyEndpoint, - Endpoint dataEndpoint, - Bundle additionalData, Parameters parameters, String productLine, String reporter) { @@ -184,10 +159,7 @@ public Parameters evaluate( periodEnd, reportType, subject, - contentEndpoint, - terminologyEndpoint, - dataEndpoint, - additionalData, + repository, parameters, productLine, reporter) @@ -210,10 +182,7 @@ public Parameters evaluate( * @param periodEnd end date of Measurement Period * @param reportType type of report * @param subject the subject ID (or practitioner) - * @param contentEndpoint content endpoint - * @param terminologyEndpoint terminology endpoint - * @param dataEndpoint data endpoint - * @param additionalData additional data bundle + * @param resolvedRepo fully configured repository (endpoints proxied, data federated) * @param parameters CQL parameters * @param productLine product line * @param reporter reporter ID @@ -226,10 +195,7 @@ MeasureDefAndR4ParametersWithMeasureReports evaluateWithDefs( @Nullable ZonedDateTime periodEnd, String reportType, String subject, - Endpoint contentEndpoint, - Endpoint terminologyEndpoint, - Endpoint dataEndpoint, - Bundle additionalData, + IRepository resolvedRepo, Parameters parameters, String productLine, String reporter) { @@ -241,10 +207,7 @@ MeasureDefAndR4ParametersWithMeasureReports evaluateWithDefs( periodEnd, reportType, subject, - contentEndpoint, - terminologyEndpoint, - dataEndpoint, - additionalData, + resolvedRepo, parameters, productLine, reporter, @@ -258,10 +221,7 @@ private List> evaluateToListOfList( @Nullable ZonedDateTime periodEnd, String reportType, String subject, - Endpoint contentEndpoint, - Endpoint terminologyEndpoint, - Endpoint dataEndpoint, - Bundle additionalData, + IRepository resolvedRepo, Parameters parameters, String productLine, String reporter, @@ -269,19 +229,8 @@ private List> evaluateToListOfList( measurePeriodValidator.validatePeriodStartAndEnd(periodStart, periodEnd); - final R4MeasureProcessor r4ProcessorToUse; - final R4MeasureServiceUtils r4MeasureServiceUtilsToUse; - if (dataEndpoint != null && contentEndpoint != null && terminologyEndpoint != null) { - var repositoryToUse = - Repositories.proxy(repository, true, dataEndpoint, contentEndpoint, terminologyEndpoint); - - r4ProcessorToUse = new R4MeasureProcessor(repositoryToUse, this.measureEvaluationOptions); - - r4MeasureServiceUtilsToUse = new R4MeasureServiceUtils(repositoryToUse); - } else { - r4ProcessorToUse = r4MeasureProcessorStandardRepository; - r4MeasureServiceUtilsToUse = r4MeasureServiceUtilsStandardRepository; - } + var r4ProcessorToUse = new R4MeasureProcessor(resolvedRepo, this.measureEvaluationOptions); + var r4MeasureServiceUtilsToUse = new R4MeasureServiceUtils(resolvedRepo); if (measureEvaluationOptions.isEnsureSearchParameters()) { r4MeasureServiceUtilsToUse.ensureSupplementalDataElementSearchParameter(); @@ -309,17 +258,13 @@ private List> evaluateToListOfList( var evalType = r4MeasureServiceUtilsToUse.getMeasureEvalType(reportType, subjectToUse); - // another flex point between single and multi measures var subjects = switch (singleOrMultiple) { - case SINGLE -> getSubjectsForEvaluateSingle(subjectToUse, repository, additionalData); + case SINGLE -> getSubjectsForEvaluateSingle(subjectToUse, resolvedRepo); case MULTIPLE -> getSubjects(subjectProvider, subjectToUse); }; - var context = Engines.forRepository( - r4ProcessorToUse.getRepository(), - this.measureEvaluationOptions.getEvaluationSettings(), - additionalData); + var context = Engines.forRepository(resolvedRepo, this.measureEvaluationOptions.getEvaluationSettings(), null); final CompositeEvaluationResultsPerMeasure compositeEvaluationResultsPerMeasure = r4ProcessorToUse.evaluateMultiMeasuresWithCqlEngine( @@ -589,19 +534,10 @@ protected Bundle.BundleEntryComponent getBundleEntry(String serverBase, Resource } @Nonnull - private List getSubjectsForEvaluateSingle( - String subjectId, IRepository proxyRepoForMeasureProcessor, Bundle additionalData) { - final IRepository repoToUseForSubjectProvider; - if (additionalData != null) { - repoToUseForSubjectProvider = new FederatedRepository( - this.repository, new InMemoryFhirRepository(this.repository.fhirContext(), additionalData)); - } else { - repoToUseForSubjectProvider = proxyRepoForMeasureProcessor; - } - + private List getSubjectsForEvaluateSingle(String subjectId, IRepository subjectRepo) { return subjectProvider .getSubjects( - repoToUseForSubjectProvider, + subjectRepo, Optional.ofNullable(subjectId).map(List::of).orElse(List.of())) .toList(); } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEnvironmentTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEnvironmentTest.java new file mode 100644 index 0000000000..7b2ba7b63c --- /dev/null +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEnvironmentTest.java @@ -0,0 +1,119 @@ +package org.opencds.cqf.fhir.cr.measure.common; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertSame; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.repository.IRepository; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Endpoint; +import org.hl7.fhir.r4.model.Patient; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.utility.repository.FederatedRepository; +import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository; +import org.opencds.cqf.fhir.utility.repository.ProxyRepository; + +/** + * Unit tests for {@link MeasureEnvironment#resolve(IRepository)}. + * + *

The critical invariant: a ProxyRepository must be created whenever any endpoint is + * provided, not only when all three are present. {@code Repositories.proxy()} already handles null + * per-endpoint entries by falling back to the local repository. + */ +class MeasureEnvironmentTest { + + private static IRepository base; + + // A real Endpoint is needed because Repositories.proxy() dispatches on fhirContext() and + // passes the endpoint to Clients.forEndpoint(). Client construction is lazy (no network call), + // so a stub address is safe in unit tests. + private static Endpoint dataEndpoint; + private static Endpoint contentEndpoint; + private static Endpoint terminologyEndpoint; + + @BeforeAll + static void setup() { + base = new InMemoryFhirRepository(FhirContext.forR4Cached()); + dataEndpoint = new Endpoint().setAddress("http://data.example.org/fhir"); + contentEndpoint = new Endpoint().setAddress("http://content.example.org/fhir"); + terminologyEndpoint = new Endpoint().setAddress("http://terminology.example.org/fhir"); + } + + // ── no-op cases ────────────────────────────────────────────────────────── + + @Test + void resolve_emptyEnvironment_returnsBaseUnchanged() { + IRepository result = MeasureEnvironment.EMPTY.resolve(base); + assertSame(base, result, "EMPTY environment should return the base repository as-is"); + } + + @Test + void resolve_allEndpointsNull_noAdditionalData_returnsBaseUnchanged() { + var env = new MeasureEnvironment(null, null, null, null); + assertSame(base, env.resolve(base)); + } + + // ── single-endpoint cases (the cases the AND bug broke) ────────────────── + + @Test + void resolve_onlyDataEndpoint_returnsProxyRepository() { + var env = new MeasureEnvironment(null, null, dataEndpoint, null); + assertInstanceOf(ProxyRepository.class, env.resolve(base)); + } + + @Test + void resolve_onlyContentEndpoint_returnsProxyRepository() { + var env = new MeasureEnvironment(contentEndpoint, null, null, null); + assertInstanceOf(ProxyRepository.class, env.resolve(base)); + } + + @Test + void resolve_onlyTerminologyEndpoint_returnsProxyRepository() { + var env = new MeasureEnvironment(null, terminologyEndpoint, null, null); + assertInstanceOf(ProxyRepository.class, env.resolve(base)); + } + + // ── all-endpoints case (worked before, must still work) ────────────────── + + @Test + void resolve_allEndpoints_returnsProxyRepository() { + var env = new MeasureEnvironment(contentEndpoint, terminologyEndpoint, dataEndpoint, null); + assertInstanceOf(ProxyRepository.class, env.resolve(base)); + } + + // ── additionalData only ─────────────────────────────────────────────────── + + @Test + void resolve_onlyAdditionalData_returnsFederatedRepository() { + Bundle bundle = bundleWithPatient("p1"); + var env = new MeasureEnvironment(null, null, null, bundle); + assertInstanceOf(FederatedRepository.class, env.resolve(base)); + } + + // ── endpoints + additionalData ──────────────────────────────────────────── + + @Test + void resolve_endpointsAndAdditionalData_returnsFederatedRepository() { + // FederatedRepository wraps the ProxyRepository; the outer type is FederatedRepository. + Bundle bundle = bundleWithPatient("p2"); + var env = new MeasureEnvironment(contentEndpoint, terminologyEndpoint, dataEndpoint, bundle); + assertInstanceOf(FederatedRepository.class, env.resolve(base)); + } + + @Test + void resolve_singleEndpointAndAdditionalData_returnsFederatedRepository() { + Bundle bundle = bundleWithPatient("p3"); + var env = new MeasureEnvironment(null, null, dataEndpoint, bundle); + assertInstanceOf(FederatedRepository.class, env.resolve(base)); + } + + // ── helpers ─────────────────────────────────────────────────────────────── + + private static Bundle bundleWithPatient(String id) { + var bundle = new Bundle(); + bundle.setType(Bundle.BundleType.COLLECTION); + bundle.addEntry().setResource(new Patient().setId(id)); + return bundle; + } +} diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java index f8db8d3025..73cf093aa4 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java @@ -25,6 +25,7 @@ import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.TERMINOLOGY_FILTER_MODE; import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings.VALUESET_EXPANSION_MODE; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; +import org.opencds.cqf.fhir.cr.measure.common.MeasureEnvironment; 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.selected.def.SelectedMeasureDef; @@ -234,10 +235,8 @@ public When evaluate() { reportType, subject, null, - null, - null, - null, - additionalData, + new MeasureEnvironment(null, null, null, additionalData) + .resolve(multiMeasureService.getRepository()), parameters, productLine, practitioner); diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MultiMeasure.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MultiMeasure.java index 181c22bef4..5b0e0f838e 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MultiMeasure.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MultiMeasure.java @@ -46,6 +46,7 @@ import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.TERMINOLOGY_FILTER_MODE; import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings.VALUESET_EXPANSION_MODE; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; +import org.opencds.cqf.fhir.cr.measure.common.MeasureEnvironment; 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.constant.MeasureConstants; @@ -256,10 +257,7 @@ public MultiMeasure.When evaluate() { periodEnd, reportType, subject, - null, - null, - null, - additionalData, + new MeasureEnvironment(null, null, null, additionalData).resolve(service.getRepository()), parameters, productLine, reporter);