From e4b4650fceea904f0ab05f839e6d90e19280dc6f Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 19 Feb 2026 16:20:46 -0500 Subject: [PATCH 1/4] Try to fix clinical intelligence repo to read partition IDs. --- cqf-fhir-benchmark/pom.xml | 8 +- cqf-fhir-bom/pom.xml | 18 +- cqf-fhir-cql/pom.xml | 8 +- cqf-fhir-cr-cli/pom.xml | 12 +- cqf-fhir-cr-hapi/pom.xml | 2 +- .../fhir/cr/hapi/config/CrCdsHooksConfig.java | 9 +- .../cr/hapi/config/test/TestCrConfig.java | 46 ++++- ...linicalIntelligenceHapiFhirRepository.java | 37 ++-- ...nicalIntelligenceRequestDetailsCloner.java | 54 +++++- ...lIntelligenceRequestDetailsClonerTest.java | 162 ++++++++++++------ cqf-fhir-cr-spring/pom.xml | 6 +- cqf-fhir-cr/pom.xml | 10 +- cqf-fhir-test/pom.xml | 4 +- cqf-fhir-utility/pom.xml | 6 +- docs/pom.xml | 6 +- pom.xml | 4 +- 16 files changed, 272 insertions(+), 120 deletions(-) diff --git a/cqf-fhir-benchmark/pom.xml b/cqf-fhir-benchmark/pom.xml index a1c59c92e8..b48271f653 100644 --- a/cqf-fhir-benchmark/pom.xml +++ b/cqf-fhir-benchmark/pom.xml @@ -5,26 +5,26 @@ org.opencds.cqf.fhir cqf-fhir-benchmark - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT FHIR Clinical Reasoning (Benchmarks) Tests validating performance of FHIR Clinical Reasoning operations org.opencds.cqf.fhir cqf-fhir - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT org.opencds.cqf.fhir cqf-fhir-cr - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT org.opencds.cqf.fhir cqf-fhir-test - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT org.openjdk.jmh diff --git a/cqf-fhir-bom/pom.xml b/cqf-fhir-bom/pom.xml index 8297564d46..7aeac648c7 100644 --- a/cqf-fhir-bom/pom.xml +++ b/cqf-fhir-bom/pom.xml @@ -5,7 +5,7 @@ org.opencds.cqf.fhir cqf-fhir-bom - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT pom FHIR Clinical Reasoning (Bill Of Materials) This bom can be used to simplify dependency management when using this project @@ -13,7 +13,7 @@ org.opencds.cqf.fhir cqf-fhir - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT @@ -21,37 +21,37 @@ org.opencds.cqf.fhir cqf-fhir-test - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT org.opencds.cqf.fhir cqf-fhir-utility - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT org.opencds.cqf.fhir cqf-fhir-cql - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT org.opencds.cqf.fhir cqf-fhir-cr - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT org.opencds.cqf.fhir cqf-fhir-cr-cli - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT org.opencds.cqf.fhir cqf-fhir-cr-hapi - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT org.opencds.cqf.fhir cqf-fhir-cr-spring - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT diff --git a/cqf-fhir-cql/pom.xml b/cqf-fhir-cql/pom.xml index 7eb51833d9..fc6ea25a52 100644 --- a/cqf-fhir-cql/pom.xml +++ b/cqf-fhir-cql/pom.xml @@ -5,21 +5,21 @@ org.opencds.cqf.fhir cqf-fhir-cql - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT FHIR Clinical Reasoning (CQL) Tools, utilities, code gen to support CQL in FHIR Clinical Reasoning operations org.opencds.cqf.fhir cqf-fhir - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT org.opencds.cqf.fhir cqf-fhir-utility - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT org.cqframework @@ -53,7 +53,7 @@ org.opencds.cqf.fhir cqf-fhir-test - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT test diff --git a/cqf-fhir-cr-cli/pom.xml b/cqf-fhir-cr-cli/pom.xml index 940ba4198e..5fcf4f406f 100644 --- a/cqf-fhir-cr-cli/pom.xml +++ b/cqf-fhir-cr-cli/pom.xml @@ -6,26 +6,26 @@ org.opencds.cqf.fhir cqf-fhir-cr-cli - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT FHIR Clinical Reasoning (CLI) CLI for running FHIR Clincial Reasoning operations org.opencds.cqf.fhir cqf-fhir - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT org.opencds.cqf.fhir cqf-fhir-utility - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT org.opencds.cqf.fhir cqf-fhir-cql - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT info.picocli @@ -58,13 +58,13 @@ org.opencds.cqf.fhir cqf-fhir-test - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT test org.opencds.cqf.fhir cqf-fhir-cr - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT compile diff --git a/cqf-fhir-cr-hapi/pom.xml b/cqf-fhir-cr-hapi/pom.xml index 57156258f2..190c8d771f 100644 --- a/cqf-fhir-cr-hapi/pom.xml +++ b/cqf-fhir-cr-hapi/pom.xml @@ -6,7 +6,7 @@ org.opencds.cqf.fhir cqf-fhir - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT cqf-fhir-cr-hapi diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/CrCdsHooksConfig.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/CrCdsHooksConfig.java index fb1171dc68..9e63a0f66f 100644 --- a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/CrCdsHooksConfig.java +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/CrCdsHooksConfig.java @@ -1,7 +1,9 @@ package org.opencds.cqf.fhir.cr.hapi.config; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry; +import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.repository.IRepository; import ca.uhn.fhir.rest.api.server.IRepositoryFactory; @@ -124,6 +126,7 @@ public CdsServiceInterceptor cdsServiceInterceptor( CdsServiceRegistryImpl cdsServiceRegistry, ICrDiscoveryServiceFactory discoveryServiceFactory, ICdsCrServiceFactory crServiceFactory, + PartitionSettings partitionSettings, Optional resourceChangeListenerRegistry) { if (resourceChangeListenerRegistry.isEmpty()) { return null; @@ -132,7 +135,11 @@ public CdsServiceInterceptor cdsServiceInterceptor( resourceChangeListenerRegistry .get() .registerResourceResourceChangeListener( - PLAN_DEFINITION_RESOURCE_NAME, SearchParameterMap.newSynchronous(), listener, 1000); + PLAN_DEFINITION_RESOURCE_NAME, + RequestPartitionId.defaultPartition(partitionSettings), + SearchParameterMap.newSynchronous(), + listener, + 1000); return listener; } } diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/test/TestCrConfig.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/test/TestCrConfig.java index 520959a60b..7c35c349a2 100644 --- a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/test/TestCrConfig.java +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/test/TestCrConfig.java @@ -1,11 +1,13 @@ package org.opencds.cqf.fhir.cr.hapi.config.test; -import ca.uhn.fhir.batch2.jobs.reindex.ReindexProvider; +import ca.uhn.fhir.batch2.jobs.bulkmodify.reindex.ReindexProvider; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; +import ca.uhn.fhir.jpa.cache.IResourceChangeListener; import ca.uhn.fhir.jpa.cache.IResourceChangeListenerCacheRefresher; import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry; import ca.uhn.fhir.jpa.cache.ResourceChangeListenerCacheFactory; @@ -13,6 +15,7 @@ import ca.uhn.fhir.jpa.cache.ResourceChangeListenerRegistryImpl; import ca.uhn.fhir.jpa.cache.ResourceChangeListenerRegistryInterceptor; import ca.uhn.fhir.jpa.graphql.GraphQLProvider; +import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.provider.DiffProvider; import ca.uhn.fhir.jpa.provider.IJpaSystemProvider; import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider; @@ -26,6 +29,7 @@ import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.provider.ResourceProviderFactory; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; +import jakarta.annotation.Nonnull; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -49,6 +53,9 @@ @Configuration @Import({SubscriptionSubmitterConfig.class, SubscriptionChannelConfig.class}) public class TestCrConfig { + public static final String LIBRARY_RESOURCE_NAME = "Library"; + public static final String VALUESET_RESOURCE_NAME = "ValueSet"; + @Bean public RestfulServer restfulServer( IFhirSystemDao fhirSystemDao, @@ -116,12 +123,13 @@ public Map> globalValueSetCache() { @Bean public ElmCacheResourceChangeListener elmCacheResourceChangeListener( IResourceChangeListenerRegistry resourceChangeListenerRegistry, + PartitionSettings partitionSettings, DaoRegistry daoRegistry, EvaluationSettings evaluationSettings) { ElmCacheResourceChangeListener listener = new ElmCacheResourceChangeListener(daoRegistry, evaluationSettings.getLibraryCache()); - resourceChangeListenerRegistry.registerResourceResourceChangeListener( - "Library", SearchParameterMap.newSynchronous(), listener, 1000); + registerResourceResourceChangeListener( + resourceChangeListenerRegistry, partitionSettings, listener, LIBRARY_RESOURCE_NAME); return listener; } @@ -129,13 +137,14 @@ public ElmCacheResourceChangeListener elmCacheResourceChangeListener( public CodeCacheResourceChangeListener codeCacheResourceChangeListener( IResourceChangeListenerRegistry resourceChangeListenerRegistry, EvaluationSettings evaluationSettings, + PartitionSettings partitionSettings, DaoRegistry daoRegistry) { CodeCacheResourceChangeListener listener = new CodeCacheResourceChangeListener(daoRegistry, evaluationSettings.getValueSetCache()); // registry - resourceChangeListenerRegistry.registerResourceResourceChangeListener( - "ValueSet", SearchParameterMap.newSynchronous(), listener, 1000); + registerResourceResourceChangeListener( + resourceChangeListenerRegistry, partitionSettings, listener, VALUESET_RESOURCE_NAME); return listener; } @@ -144,9 +153,10 @@ public CodeCacheResourceChangeListener codeCacheResourceChangeListener( public IResourceChangeListenerRegistry resourceChangeListenerRegistry( InMemoryResourceMatcher inMemoryResourceMatcher, FhirContext fhirContext, + PartitionSettings partitionSettings, ResourceChangeListenerCacheFactory resourceChangeListenerCacheFactory) { return new ResourceChangeListenerRegistryImpl( - fhirContext, resourceChangeListenerCacheFactory, inMemoryResourceMatcher); + fhirContext, partitionSettings, resourceChangeListenerCacheFactory, inMemoryResourceMatcher); } @Bean @@ -158,4 +168,28 @@ IResourceChangeListenerCacheRefresher resourceChangeListenerCacheRefresher() { public ResourceChangeListenerRegistryInterceptor resourceChangeListenerRegistryInterceptor() { return new ResourceChangeListenerRegistryInterceptor(); } + + @Bean + public PartitionSettings partitionSettings() { + return new PartitionSettings(); + } + + private void registerResourceResourceChangeListener( + IResourceChangeListenerRegistry resourceChangeListenerRegistry, + PartitionSettings partitionSettings, + IResourceChangeListener listener, + String resourceType) { + + resourceChangeListenerRegistry.registerResourceResourceChangeListener( + resourceType, + getRequestPartitionId(partitionSettings), + SearchParameterMap.newSynchronous(), + listener, + 1000); + } + + @Nonnull + private RequestPartitionId getRequestPartitionId(PartitionSettings partitionSettings) { + return RequestPartitionId.defaultPartition(partitionSettings); + } } diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceHapiFhirRepository.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceHapiFhirRepository.java index 885757f5db..cf66af37cf 100644 --- a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceHapiFhirRepository.java +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceHapiFhirRepository.java @@ -6,10 +6,10 @@ import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.repository.HapiFhirRepository; -import ca.uhn.fhir.jpa.repository.SearchConverter; -import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.jpa.repository.searchparam.SearchParameterMapRepositoryRestQueryBuilder; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.valueset.BundleTypeEnum; +import ca.uhn.fhir.repository.impl.MultiMapRepositoryRestQueryBuilder; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; @@ -18,14 +18,13 @@ import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServerUtils; import ca.uhn.fhir.rest.server.SimpleBundleProvider; -import com.google.common.collect.Multimap; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,9 +36,10 @@ * Since hapi-fhir is released quarterly, we want to maintain flexibility to change behaviour * within our more flexible release cadence. */ +@SuppressWarnings("UnstableApiUsage") public class ClinicalIntelligenceHapiFhirRepository extends HapiFhirRepository { - private static final Logger ourLog = LoggerFactory.getLogger(ClinicalIntelligenceHapiFhirRepository.class); + private static final Logger logger = LoggerFactory.getLogger(ClinicalIntelligenceHapiFhirRepository.class); private final RequestDetails requestDetails; private final RestfulServer restfulServer; @@ -53,8 +53,19 @@ public ClinicalIntelligenceHapiFhirRepository( this.daoRegistry = daoRegistry; } + @Override + public T read( + Class resourceType, I id, Map headers) { + + var details = ClinicalIntelligenceRequestDetailsCloner.startWith(requestDetails, resourceType) + .setAction(RestOperationTypeEnum.READ) + .addHeaders(headers) + .create(); + return (T) this.daoRegistry.getResourceDao(resourceType).read(id, details); + } + /** - * Override {@link HapiFhirRepository#search(Class, Class, Map, Map)} to ensure that the + * Override {@link HapiFhirRepository#search(Class, Class, IRepositoryRestQueryContributor, Map)} to ensure that the * _count {@link RequestDetails} parameter is passed through to the DAO layer instead of * dropping it. *

@@ -67,18 +78,18 @@ public ClinicalIntelligenceHapiFhirRepository( public B search( Class bundleType, Class resourceType, - Multimap> searchParameters, + IRepositoryRestQueryContributor queryContributor, Map headers) { - var details = ClinicalIntelligenceRequestDetailsCloner.startWith(requestDetails) + var details = ClinicalIntelligenceRequestDetailsCloner.startWith(requestDetails, resourceType) .setAction(RestOperationTypeEnum.SEARCH_TYPE) .addHeaders(headers) .create(); - var converter = new SearchConverter(); - converter.convertParameters(searchParameters, fhirContext()); - details.setParameters(converter.myResultParameters); + var searchParameterMap = + SearchParameterMapRepositoryRestQueryBuilder.buildFromQueryContributor(queryContributor); + details.setParameters(MultiMapRepositoryRestQueryBuilder.toFlatMap(searchParameterMap)); details.setResourceName(daoRegistry.getFhirContext().getResourceType(resourceType)); if (details instanceof SystemRequestDetails) { @@ -89,7 +100,7 @@ public B search( } var resourceDao = daoRegistry.getResourceDao(resourceType); - var bundleProvider = resourceDao.search(converter.mySearchParameterMap, details); + var bundleProvider = resourceDao.search(searchParameterMap, details); bundleProvider = sanitizeBundleProvider(bundleProvider); @@ -125,7 +136,7 @@ protected BundleTypeEnum extractBundleTypeFromRequestParameters(Map DetailsBuilder startWith( + RequestDetails origRequestDetails, Class resourceType) { + final RequestDetails newDetails; + if (origRequestDetails instanceof ServletRequestDetails servletDetails) { newDetails = new ServletRequestDetails(servletDetails); + } else if (origRequestDetails instanceof SystemRequestDetails systemRequestDetails) { + final SystemRequestDetails clonedSystemRequestDetails = new SystemRequestDetails(origRequestDetails); + if (isPartitionableResource(resourceType)) { + clonedSystemRequestDetails.setRequestPartitionId(systemRequestDetails.getRequestPartitionId()); + } + newDetails = clonedSystemRequestDetails; } else { - newDetails = new SystemRequestDetails(details); + throw new InvalidRequestException("Unsupported request origRequestDetails type: %s" + .formatted(origRequestDetails.getClass().getName())); + } + if (isPartitionableResource(resourceType)) { + newDetails.setTenantId(origRequestDetails.getTenantId()); } newDetails.setRequestType(RequestTypeEnum.POST); newDetails.setOperation(null); newDetails.setResource(null); - newDetails.setParameters(new HashMap<>(details.getParameters())); + newDetails.setParameters(new HashMap<>(origRequestDetails.getParameters())); newDetails.setResourceName(null); newDetails.setCompartmentName(null); - newDetails.setResponse(details.getResponse()); + newDetails.setResponse(origRequestDetails.getResponse()); return new DetailsBuilder(newDetails); } + // LUKETODO: this logic is maintained in BaseRequestPartitionHelperSvc#isResourcePartitionable + private static boolean isPartitionableResource(Class resourceType) { + final String requestResourceName = resourceType.getName(); + + // LUKETODO: Measure isn't in here??? WTF? + final Set nonPartitionableResourceTypes = Stream.of( + ResourceType.Library, + ResourceType.ValueSet, + ResourceType.StructureMap, + ResourceType.StructureDefinition, + ResourceType.Questionnaire, + ResourceType.NamingSystem, + ResourceType.CompartmentDefinition, + ResourceType.SearchParameter, + ResourceType.ConceptMap, + ResourceType.OperationDefinition, + ResourceType.CodeSystem) + .map(Enum::name) + .collect(Collectors.toUnmodifiableSet()); + + final boolean isResourceNonPartitionable = nonPartitionableResourceTypes.contains(requestResourceName); + + return !isResourceNonPartitionable; + } + static class DetailsBuilder { private final RequestDetails details; diff --git a/cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceRequestDetailsClonerTest.java b/cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceRequestDetailsClonerTest.java index 98a79282ab..7f135bde10 100644 --- a/cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceRequestDetailsClonerTest.java +++ b/cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceRequestDetailsClonerTest.java @@ -1,97 +1,155 @@ package org.opencds.cqf.fhir.cr.hapi.repository; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.server.IRestfulResponse; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.api.server.SystemRestfulResponse; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import java.util.HashMap; import java.util.Map; +import java.util.stream.Stream; +import org.hl7.fhir.r4.model.Measure; import org.junit.jupiter.api.Test; -import org.opencds.cqf.fhir.cr.hapi.repository.ClinicalIntelligenceRequestDetailsCloner.DetailsBuilder; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; class ClinicalIntelligenceRequestDetailsClonerTest { - @Test - void testStartWithServletRequestDetails() { - ServletRequestDetails mockDetails = mock(ServletRequestDetails.class); - Map mockParams = new HashMap<>(); - mockParams.put("key1", new String[] {"value1"}); - when(mockDetails.getParameters()).thenReturn(mockParams); + record RequestDetailsVariant(String label, RequestDetails requestDetails) { + @Override + public String toString() { + return label; + } + } + + static Stream requestDetailsVariants() { + var system = new SystemRequestDetails(); + system.setParameters(new HashMap<>()); - DetailsBuilder builder = ClinicalIntelligenceRequestDetailsCloner.startWith(mockDetails); - RequestDetails result = builder.create(); + var servlet = new ServletRequestDetails(); + servlet.setParameters(new HashMap<>()); + + return Stream.of( + new RequestDetailsVariant("SystemRequestDetails", system), + new RequestDetailsVariant("ServletRequestDetails", servlet)); + } + + @ParameterizedTest + @MethodSource("requestDetailsVariants") + void startWith_SetsRequestTypeToPost(RequestDetailsVariant variant) { + RequestDetails result = ClinicalIntelligenceRequestDetailsCloner.startWith( + variant.requestDetails(), Measure.class) + .create(); - assertNotNull(result); assertEquals(RequestTypeEnum.POST, result.getRequestType()); - assertNull(result.getResource()); + } + + @ParameterizedTest + @MethodSource("requestDetailsVariants") + void startWith_ResetsOperationResourceAndNames(RequestDetailsVariant variant) { + RequestDetails result = ClinicalIntelligenceRequestDetailsCloner.startWith( + variant.requestDetails(), Measure.class) + .create(); + assertNull(result.getOperation()); - assertEquals(mockParams, result.getParameters()); + assertNull(result.getResource()); + assertNull(result.getResourceName()); + assertNull(result.getCompartmentName()); } - @Test - void testStartWithSystemRequestDetails() { - RequestDetails mockDetails = mock(SystemRequestDetails.class); - Map mockParams = new HashMap<>(); - mockParams.put("key2", new String[] {"value2"}); - when(mockDetails.getParameters()).thenReturn(mockParams); + @ParameterizedTest + @MethodSource("requestDetailsVariants") + void startWith_PreservesTenantId(RequestDetailsVariant variant) { + variant.requestDetails().setTenantId("test-tenant"); - DetailsBuilder builder = ClinicalIntelligenceRequestDetailsCloner.startWith(mockDetails); - RequestDetails result = builder.create(); + RequestDetails result = ClinicalIntelligenceRequestDetailsCloner.startWith( + variant.requestDetails(), Measure.class) + .create(); - assertNotNull(result); - assertEquals(RequestTypeEnum.POST, result.getRequestType()); - assertNull(result.getResource()); - assertNull(result.getOperation()); - assertEquals(mockParams, result.getParameters()); + assertEquals("test-tenant", result.getTenantId()); } - @Test - void testStartWithSetsResponseFromOriginalRequest() { - RequestDetails mockDetails = mock(RequestDetails.class); + @ParameterizedTest + @MethodSource("requestDetailsVariants") + void startWith_PreservesResponse(RequestDetailsVariant variant) { IRestfulResponse expectedResponse = new SystemRestfulResponse(null); - when(mockDetails.getResponse()).thenReturn(expectedResponse); + variant.requestDetails().setResponse(expectedResponse); - DetailsBuilder builder = ClinicalIntelligenceRequestDetailsCloner.startWith(mockDetails); - RequestDetails result = builder.create(); + RequestDetails result = ClinicalIntelligenceRequestDetailsCloner.startWith( + variant.requestDetails(), Measure.class) + .create(); - assertNotNull(result); assertEquals(expectedResponse, result.getResponse()); } + @ParameterizedTest + @MethodSource("requestDetailsVariants") + void startWith_ClonesParameters(RequestDetailsVariant variant) { + Map originalParams = new HashMap<>(); + originalParams.put("param1", new String[] {"value1", "value2"}); + variant.requestDetails().setParameters(originalParams); + + RequestDetails result = ClinicalIntelligenceRequestDetailsCloner.startWith( + variant.requestDetails(), Measure.class) + .create(); + + assertNotSame(originalParams, result.getParameters()); + assertEquals(originalParams, result.getParameters()); + } + + @ParameterizedTest + @MethodSource("requestDetailsVariants") + void startWith_PreservesRequestDetailsType(RequestDetailsVariant variant) { + RequestDetails result = ClinicalIntelligenceRequestDetailsCloner.startWith( + variant.requestDetails(), Measure.class) + .create(); + + assertInstanceOf(variant.requestDetails().getClass(), result); + } + @Test - void testStartWithResetsResourceNameAndCompartmentName() { - RequestDetails mockDetails = mock(RequestDetails.class); + void startWith_SystemRequestDetails_PreservesRequestPartitionId() { + var partitionId = RequestPartitionId.fromPartitionId(123); + var system = new SystemRequestDetails(); + system.setParameters(new HashMap<>()); + system.setRequestPartitionId(partitionId); - DetailsBuilder builder = ClinicalIntelligenceRequestDetailsCloner.startWith(mockDetails); - RequestDetails result = builder.create(); + RequestDetails result = ClinicalIntelligenceRequestDetailsCloner.startWith(system, Measure.class) + .create(); - assertNotNull(result); - assertNull(result.getResourceName()); - assertNull(result.getCompartmentName()); + assertInstanceOf(SystemRequestDetails.class, result); + assertEquals(partitionId, ((SystemRequestDetails) result).getRequestPartitionId()); } @Test - void testStartWithClonesParametersCorrectly() { - RequestDetails mockDetails = mock(RequestDetails.class); - Map originalParams = new HashMap<>(); - originalParams.put("param1", new String[] {"value1", "value2"}); - when(mockDetails.getParameters()).thenReturn(originalParams); + void startWith_SystemRequestDetails_PreservesNullRequestPartitionId() { + var system = new SystemRequestDetails(); + system.setParameters(new HashMap<>()); - DetailsBuilder builder = ClinicalIntelligenceRequestDetailsCloner.startWith(mockDetails); - RequestDetails result = builder.create(); + RequestDetails result = ClinicalIntelligenceRequestDetailsCloner.startWith(system, Measure.class) + .create(); + + assertInstanceOf(SystemRequestDetails.class, result); + assertNull(((SystemRequestDetails) result).getRequestPartitionId()); + } + + @Test + void startWith_UnsupportedType_ThrowsInvalidRequestException() { + // Mock creates a proxy that is neither SystemRequestDetails nor ServletRequestDetails + RequestDetails unsupported = mock(RequestDetails.class); - assertNotNull(result); - Map clonedParams = result.getParameters(); - assertNotSame(originalParams, clonedParams); - assertEquals(originalParams, clonedParams); + assertThrows( + InvalidRequestException.class, + () -> ClinicalIntelligenceRequestDetailsCloner.startWith(unsupported, Measure.class)); } } diff --git a/cqf-fhir-cr-spring/pom.xml b/cqf-fhir-cr-spring/pom.xml index 7aac5627d9..6af2508c2a 100644 --- a/cqf-fhir-cr-spring/pom.xml +++ b/cqf-fhir-cr-spring/pom.xml @@ -7,21 +7,21 @@ org.opencds.cqf.fhir cqf-fhir-cr-spring - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT FHIR Clinical Reasoning (Spring) Spring configurations for FHIR Clinical Reasoning org.opencds.cqf.fhir cqf-fhir - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT org.opencds.cqf.fhir cqf-fhir-cr - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT org.springframework diff --git a/cqf-fhir-cr/pom.xml b/cqf-fhir-cr/pom.xml index e75f1e14c2..81c4f6e4e0 100644 --- a/cqf-fhir-cr/pom.xml +++ b/cqf-fhir-cr/pom.xml @@ -5,33 +5,33 @@ org.opencds.cqf.fhir cqf-fhir-cr - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT FHIR Clinical Reasoning (Operations) Implementations of clinical reasoning operations org.opencds.cqf.fhir cqf-fhir - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT org.opencds.cqf.fhir cqf-fhir-cql - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT org.opencds.cqf.fhir cqf-fhir-utility - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT org.opencds.cqf.fhir cqf-fhir-test - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT test diff --git a/cqf-fhir-test/pom.xml b/cqf-fhir-test/pom.xml index 958657b58e..c8baea7b88 100644 --- a/cqf-fhir-test/pom.xml +++ b/cqf-fhir-test/pom.xml @@ -5,14 +5,14 @@ org.opencds.cqf.fhir cqf-fhir-test - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT FHIR Clinical Reasoning (Test Utilities) Utilities to support unit testing clinical reasoning operations org.opencds.cqf.fhir cqf-fhir - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT diff --git a/cqf-fhir-utility/pom.xml b/cqf-fhir-utility/pom.xml index 58667d2f68..b85307a357 100644 --- a/cqf-fhir-utility/pom.xml +++ b/cqf-fhir-utility/pom.xml @@ -5,14 +5,14 @@ org.opencds.cqf.fhir cqf-fhir-utility - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT FHIR Clinical Reasoning (Utilities) Utilities to help develop clinical reasoning operations org.opencds.cqf.fhir cqf-fhir - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT @@ -94,7 +94,7 @@ org.opencds.cqf.fhir cqf-fhir-test - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT test diff --git a/docs/pom.xml b/docs/pom.xml index d37b024870..ac096c8b45 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -5,7 +5,7 @@ org.opencds.cqf.fhir docs - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT FHIR Clinical Reasoning (Documentation) Documentation website for FHIR Clinical Reasoning @@ -13,7 +13,7 @@ org.opencds.cqf.fhir cqf-fhir - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT @@ -23,7 +23,7 @@ cqf-fhir-bom pom import - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT diff --git a/pom.xml b/pom.xml index ec1e1d542a..da858a235d 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.opencds.cqf.fhir cqf-fhir - 4.3.0-SNAPSHOT + 4.4.0-SNAPSHOT pom FHIR Clinical Reasoning FHIR Clinical Reasoning Implementations @@ -25,7 +25,7 @@ 4.2.0 1.36 6.2.12 - 8.6.0 + 8.7.13-SNAPSHOT 4.6.1 33.2.1-jre 2.35.1 From 83a16f298fefd5842d5f7cb78802653c5009dd48 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Thu, 19 Feb 2026 19:01:37 -0500 Subject: [PATCH 2/4] Fix isPartitionable logic in ClinicalIntelligenceRequestDetailsCloner and add testing. Ensure that resource names are derived from FhirContext. Add Claude-generated tests. --- ...linicalIntelligenceHapiFhirRepository.java | 6 +- ...nicalIntelligenceRequestDetailsCloner.java | 20 ++- ...lIntelligenceRequestDetailsClonerTest.java | 152 ++++++++++++++++-- 3 files changed, 159 insertions(+), 19 deletions(-) diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceHapiFhirRepository.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceHapiFhirRepository.java index cf66af37cf..9d4eddb13e 100644 --- a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceHapiFhirRepository.java +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceHapiFhirRepository.java @@ -57,7 +57,8 @@ public ClinicalIntelligenceHapiFhirRepository( public T read( Class resourceType, I id, Map headers) { - var details = ClinicalIntelligenceRequestDetailsCloner.startWith(requestDetails, resourceType) + var details = ClinicalIntelligenceRequestDetailsCloner.startWith( + requestDetails, daoRegistry.getFhirContext(), resourceType) .setAction(RestOperationTypeEnum.READ) .addHeaders(headers) .create(); @@ -81,7 +82,8 @@ public B search( IRepositoryRestQueryContributor queryContributor, Map headers) { - var details = ClinicalIntelligenceRequestDetailsCloner.startWith(requestDetails, resourceType) + var details = ClinicalIntelligenceRequestDetailsCloner.startWith( + requestDetails, daoRegistry.getFhirContext(), resourceType) .setAction(RestOperationTypeEnum.SEARCH_TYPE) .addHeaders(headers) .create(); diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceRequestDetailsCloner.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceRequestDetailsCloner.java index 3e713fe639..1bf4f28ba5 100644 --- a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceRequestDetailsCloner.java +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceRequestDetailsCloner.java @@ -1,5 +1,6 @@ package org.opencds.cqf.fhir.cr.hapi.repository; +import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; @@ -24,13 +25,16 @@ class ClinicalIntelligenceRequestDetailsCloner { private ClinicalIntelligenceRequestDetailsCloner() {} static DetailsBuilder startWith( - RequestDetails origRequestDetails, Class resourceType) { + RequestDetails origRequestDetails, FhirContext fhirContext, Class resourceType) { final RequestDetails newDetails; + + final boolean isPartitionableResource = isPartitionableResource(fhirContext, resourceType); + if (origRequestDetails instanceof ServletRequestDetails servletDetails) { newDetails = new ServletRequestDetails(servletDetails); } else if (origRequestDetails instanceof SystemRequestDetails systemRequestDetails) { final SystemRequestDetails clonedSystemRequestDetails = new SystemRequestDetails(origRequestDetails); - if (isPartitionableResource(resourceType)) { + if (isPartitionableResource) { clonedSystemRequestDetails.setRequestPartitionId(systemRequestDetails.getRequestPartitionId()); } newDetails = clonedSystemRequestDetails; @@ -38,7 +42,7 @@ static DetailsBuilder startWith( throw new InvalidRequestException("Unsupported request origRequestDetails type: %s" .formatted(origRequestDetails.getClass().getName())); } - if (isPartitionableResource(resourceType)) { + if (isPartitionableResource) { newDetails.setTenantId(origRequestDetails.getTenantId()); } newDetails.setRequestType(RequestTypeEnum.POST); @@ -53,10 +57,8 @@ static DetailsBuilder startWith( } // LUKETODO: this logic is maintained in BaseRequestPartitionHelperSvc#isResourcePartitionable - private static boolean isPartitionableResource(Class resourceType) { - final String requestResourceName = resourceType.getName(); - - // LUKETODO: Measure isn't in here??? WTF? + private static boolean isPartitionableResource( + FhirContext fhirContext, Class resourceType) { final Set nonPartitionableResourceTypes = Stream.of( ResourceType.Library, ResourceType.ValueSet, @@ -72,7 +74,9 @@ private static boolean isPartitionableResource(Class requestDetailsVariants() { @MethodSource("requestDetailsVariants") void startWith_SetsRequestTypeToPost(RequestDetailsVariant variant) { RequestDetails result = ClinicalIntelligenceRequestDetailsCloner.startWith( - variant.requestDetails(), Measure.class) + variant.requestDetails(), FHIR_CONTEXT, Measure.class) .create(); assertEquals(RequestTypeEnum.POST, result.getRequestType()); @@ -58,7 +62,7 @@ void startWith_SetsRequestTypeToPost(RequestDetailsVariant variant) { @MethodSource("requestDetailsVariants") void startWith_ResetsOperationResourceAndNames(RequestDetailsVariant variant) { RequestDetails result = ClinicalIntelligenceRequestDetailsCloner.startWith( - variant.requestDetails(), Measure.class) + variant.requestDetails(), FHIR_CONTEXT, Measure.class) .create(); assertNull(result.getOperation()); @@ -73,7 +77,7 @@ void startWith_PreservesTenantId(RequestDetailsVariant variant) { variant.requestDetails().setTenantId("test-tenant"); RequestDetails result = ClinicalIntelligenceRequestDetailsCloner.startWith( - variant.requestDetails(), Measure.class) + variant.requestDetails(), FHIR_CONTEXT, Measure.class) .create(); assertEquals("test-tenant", result.getTenantId()); @@ -86,7 +90,7 @@ void startWith_PreservesResponse(RequestDetailsVariant variant) { variant.requestDetails().setResponse(expectedResponse); RequestDetails result = ClinicalIntelligenceRequestDetailsCloner.startWith( - variant.requestDetails(), Measure.class) + variant.requestDetails(), FHIR_CONTEXT, Measure.class) .create(); assertEquals(expectedResponse, result.getResponse()); @@ -100,7 +104,7 @@ void startWith_ClonesParameters(RequestDetailsVariant variant) { variant.requestDetails().setParameters(originalParams); RequestDetails result = ClinicalIntelligenceRequestDetailsCloner.startWith( - variant.requestDetails(), Measure.class) + variant.requestDetails(), FHIR_CONTEXT, Measure.class) .create(); assertNotSame(originalParams, result.getParameters()); @@ -111,7 +115,7 @@ void startWith_ClonesParameters(RequestDetailsVariant variant) { @MethodSource("requestDetailsVariants") void startWith_PreservesRequestDetailsType(RequestDetailsVariant variant) { RequestDetails result = ClinicalIntelligenceRequestDetailsCloner.startWith( - variant.requestDetails(), Measure.class) + variant.requestDetails(), FHIR_CONTEXT, Measure.class) .create(); assertInstanceOf(variant.requestDetails().getClass(), result); @@ -124,7 +128,7 @@ void startWith_SystemRequestDetails_PreservesRequestPartitionId() { system.setParameters(new HashMap<>()); system.setRequestPartitionId(partitionId); - RequestDetails result = ClinicalIntelligenceRequestDetailsCloner.startWith(system, Measure.class) + RequestDetails result = ClinicalIntelligenceRequestDetailsCloner.startWith(system, FHIR_CONTEXT, Measure.class) .create(); assertInstanceOf(SystemRequestDetails.class, result); @@ -136,7 +140,7 @@ void startWith_SystemRequestDetails_PreservesNullRequestPartitionId() { var system = new SystemRequestDetails(); system.setParameters(new HashMap<>()); - RequestDetails result = ClinicalIntelligenceRequestDetailsCloner.startWith(system, Measure.class) + RequestDetails result = ClinicalIntelligenceRequestDetailsCloner.startWith(system, FHIR_CONTEXT, Measure.class) .create(); assertInstanceOf(SystemRequestDetails.class, result); @@ -150,6 +154,136 @@ void startWith_UnsupportedType_ThrowsInvalidRequestException() { assertThrows( InvalidRequestException.class, - () -> ClinicalIntelligenceRequestDetailsCloner.startWith(unsupported, Measure.class)); + () -> ClinicalIntelligenceRequestDetailsCloner.startWith(unsupported, FHIR_CONTEXT, Measure.class)); + } + + record PartitionableResourceVariant( + String label, + FhirContext fhirContext, + Class resourceType, + boolean expectedPartitionable) { + @Override + public String toString() { + return label; + } + } + + static Stream partitionableResourceVariants() { + return Stream.of( + // R4 partitionable resources + new PartitionableResourceVariant( + "R4 Patient", FhirContext.forR4Cached(), org.hl7.fhir.r4.model.Patient.class, true), + new PartitionableResourceVariant( + "R4 Observation", FhirContext.forR4Cached(), org.hl7.fhir.r4.model.Observation.class, true), + new PartitionableResourceVariant( + "R4 Measure", FhirContext.forR4Cached(), org.hl7.fhir.r4.model.Measure.class, true), + new PartitionableResourceVariant( + "R4 Encounter", FhirContext.forR4Cached(), org.hl7.fhir.r4.model.Encounter.class, true), + // R4 non-partitionable resources (all 11 types) + new PartitionableResourceVariant( + "R4 Library", FhirContext.forR4Cached(), org.hl7.fhir.r4.model.Library.class, false), + new PartitionableResourceVariant( + "R4 ValueSet", FhirContext.forR4Cached(), org.hl7.fhir.r4.model.ValueSet.class, false), + new PartitionableResourceVariant( + "R4 StructureMap", FhirContext.forR4Cached(), org.hl7.fhir.r4.model.StructureMap.class, false), + new PartitionableResourceVariant( + "R4 StructureDefinition", + FhirContext.forR4Cached(), + org.hl7.fhir.r4.model.StructureDefinition.class, + false), + new PartitionableResourceVariant( + "R4 Questionnaire", + FhirContext.forR4Cached(), + org.hl7.fhir.r4.model.Questionnaire.class, + false), + new PartitionableResourceVariant( + "R4 NamingSystem", FhirContext.forR4Cached(), org.hl7.fhir.r4.model.NamingSystem.class, false), + new PartitionableResourceVariant( + "R4 CompartmentDefinition", + FhirContext.forR4Cached(), + org.hl7.fhir.r4.model.CompartmentDefinition.class, + false), + new PartitionableResourceVariant( + "R4 SearchParameter", + FhirContext.forR4Cached(), + org.hl7.fhir.r4.model.SearchParameter.class, + false), + new PartitionableResourceVariant( + "R4 ConceptMap", FhirContext.forR4Cached(), org.hl7.fhir.r4.model.ConceptMap.class, false), + new PartitionableResourceVariant( + "R4 OperationDefinition", + FhirContext.forR4Cached(), + org.hl7.fhir.r4.model.OperationDefinition.class, + false), + new PartitionableResourceVariant( + "R4 CodeSystem", FhirContext.forR4Cached(), org.hl7.fhir.r4.model.CodeSystem.class, false), + // DSTU3 partitionable + new PartitionableResourceVariant( + "DSTU3 Patient", FhirContext.forDstu3Cached(), org.hl7.fhir.dstu3.model.Patient.class, true), + new PartitionableResourceVariant( + "DSTU3 Observation", + FhirContext.forDstu3Cached(), + org.hl7.fhir.dstu3.model.Observation.class, + true), + // DSTU3 non-partitionable + new PartitionableResourceVariant( + "DSTU3 Library", FhirContext.forDstu3Cached(), org.hl7.fhir.dstu3.model.Library.class, false), + new PartitionableResourceVariant( + "DSTU3 ValueSet", FhirContext.forDstu3Cached(), org.hl7.fhir.dstu3.model.ValueSet.class, false), + new PartitionableResourceVariant( + "DSTU3 CodeSystem", + FhirContext.forDstu3Cached(), + org.hl7.fhir.dstu3.model.CodeSystem.class, + false), + // R5 partitionable + new PartitionableResourceVariant( + "R5 Patient", FhirContext.forR5Cached(), org.hl7.fhir.r5.model.Patient.class, true), + new PartitionableResourceVariant( + "R5 Observation", FhirContext.forR5Cached(), org.hl7.fhir.r5.model.Observation.class, true), + // R5 non-partitionable + new PartitionableResourceVariant( + "R5 Library", FhirContext.forR5Cached(), org.hl7.fhir.r5.model.Library.class, false), + new PartitionableResourceVariant( + "R5 ValueSet", FhirContext.forR5Cached(), org.hl7.fhir.r5.model.ValueSet.class, false), + new PartitionableResourceVariant( + "R5 CodeSystem", FhirContext.forR5Cached(), org.hl7.fhir.r5.model.CodeSystem.class, false)); + } + + @ParameterizedTest + @MethodSource("partitionableResourceVariants") + void startWith_SystemRequestDetails_PreservesPartitionIdOnlyForPartitionableResource( + PartitionableResourceVariant variant) { + var partitionId = RequestPartitionId.fromPartitionId(123); + var system = new SystemRequestDetails(); + system.setParameters(new HashMap<>()); + system.setRequestPartitionId(partitionId); + + RequestDetails result = ClinicalIntelligenceRequestDetailsCloner.startWith( + system, variant.fhirContext(), variant.resourceType()) + .create(); + + assertInstanceOf(SystemRequestDetails.class, result); + if (variant.expectedPartitionable()) { + assertEquals(partitionId, ((SystemRequestDetails) result).getRequestPartitionId()); + } else { + assertNull(((SystemRequestDetails) result).getRequestPartitionId()); + } + } + + @ParameterizedTest + @MethodSource("partitionableResourceVariants") + void startWith_SystemRequestDetails_PreservesTenantIdRegardlessOfPartitionability( + PartitionableResourceVariant variant) { + // Tenant ID is always preserved for SystemRequestDetails because the + // constructor copies it, regardless of whether the resource is partitionable + var system = new SystemRequestDetails(); + system.setParameters(new HashMap<>()); + system.setTenantId("test-tenant"); + + RequestDetails result = ClinicalIntelligenceRequestDetailsCloner.startWith( + system, variant.fhirContext(), variant.resourceType()) + .create(); + + assertEquals("test-tenant", result.getTenantId()); } } From b4f2568c2cf99191a6fec6d9ad7a6f1e4fa435ec Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 20 Feb 2026 11:27:35 -0500 Subject: [PATCH 3/4] Ensure more repository method calls copy over partitioning info. --- ...linicalIntelligenceHapiFhirRepository.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceHapiFhirRepository.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceHapiFhirRepository.java index 9d4eddb13e..b5c1ae5853 100644 --- a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceHapiFhirRepository.java +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceHapiFhirRepository.java @@ -5,12 +5,15 @@ import static java.util.Objects.nonNull; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.repository.HapiFhirRepository; import ca.uhn.fhir.jpa.repository.searchparam.SearchParameterMapRepositoryRestQueryBuilder; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.valueset.BundleTypeEnum; import ca.uhn.fhir.repository.impl.MultiMapRepositoryRestQueryBuilder; import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.PatchTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; @@ -23,6 +26,7 @@ import java.util.Map.Entry; import java.util.Set; import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.slf4j.Logger; @@ -109,6 +113,48 @@ public B search( return createBundle(details, bundleProvider); } + public MethodOutcome create(T resource, Map headers) { + return this.daoRegistry + .getResourceDao(resource) + .create(resource, cloneWithAction(resource, headers, RestOperationTypeEnum.CREATE)); + } + + public MethodOutcome update(T resource, Map headers) { + final DaoMethodOutcome update = daoRegistry + .getResourceDao(resource) + .update(resource, cloneWithAction(resource, headers, RestOperationTypeEnum.UPDATE)); + boolean created = update.getCreated() != null && update.getCreated(); + if (created) { + update.setResponseStatusCode(201); + } else { + update.setResponseStatusCode(200); + } + + return update; + } + + public MethodOutcome patch( + I id, P patchParameters, Map headers) { + return this.daoRegistry + .getResourceDao(id.getResourceType()) + .patch( + id, + null, + PatchTypeEnum.FHIR_PATCH_JSON, + null, + patchParameters, + cloneWithAction(patchParameters, headers, RestOperationTypeEnum.PATCH)); + } + + public MethodOutcome delete( + Class resourceType, I id, Map headers) { + return this.daoRegistry + .getResourceDao(resourceType) + .delete(id, cloneWithAction(resourceType, headers, RestOperationTypeEnum.DELETE)); + } + + // N.B. We don't support request details cloning for link + protected IBundleProvider sanitizeBundleProvider(IBundleProvider bundleProvider) { return nonNull(bundleProvider) ? bundleProvider : new SimpleBundleProvider(); } @@ -148,6 +194,21 @@ protected BundleTypeEnum extractBundleTypeFromRequestParameters(Map RequestDetails cloneWithAction( + T resource, Map headers, RestOperationTypeEnum operationTypeEnum) { + return cloneWithAction(resource.getClass(), headers, operationTypeEnum); + } + + private RequestDetails cloneWithAction( + Class resourceType, Map headers, RestOperationTypeEnum operationTypeEnum) { + + return ClinicalIntelligenceRequestDetailsCloner.startWith( + requestDetails, daoRegistry.getFhirContext(), resourceType) + .setAction(operationTypeEnum) + .addHeaders(headers) + .create(); + } + private B createBundle(RequestDetails requestDetails, IBundleProvider bundleProvider) { Integer count = RestfulServerUtils.extractCountParameter(requestDetails); From 8a748d9c5063c3598b22f3d0af7150c7d0f18449 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 20 Feb 2026 12:31:48 -0500 Subject: [PATCH 4/4] Add test changes. --- ...ceHapiFhirRepositoryDaoOperationsTest.java | 284 ++++++++++++++++++ ...lIntelligenceRequestDetailsClonerTest.java | 5 +- 2 files changed, 287 insertions(+), 2 deletions(-) create mode 100644 cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceHapiFhirRepositoryDaoOperationsTest.java diff --git a/cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceHapiFhirRepositoryDaoOperationsTest.java b/cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceHapiFhirRepositoryDaoOperationsTest.java new file mode 100644 index 0000000000..9d9a1f49f7 --- /dev/null +++ b/cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceHapiFhirRepositoryDaoOperationsTest.java @@ -0,0 +1,284 @@ +package org.opencds.cqf.fhir.cr.hapi.repository; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; +import ca.uhn.fhir.rest.api.PatchTypeEnum; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.rest.server.RestfulServer; +import java.util.Map; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Library; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Patient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +class ClinicalIntelligenceHapiFhirRepositoryDaoOperationsTest { + + private static final FhirContext FHIR_CONTEXT = FhirContext.forR4Cached(); + private static final String TENANT_ID = "test-tenant"; + private static final RequestPartitionId PARTITION_ID = RequestPartitionId.fromPartitionId(123); + + @Mock + private DaoRegistry daoRegistry; + + @Mock + private IFhirResourceDao patientDao; + + @Mock + private IFhirResourceDao libraryDao; + + private AutoCloseable mocks; + + @BeforeEach + void setUp() { + mocks = MockitoAnnotations.openMocks(this); + lenient().when(daoRegistry.getFhirContext()).thenReturn(FHIR_CONTEXT); + } + + @AfterEach + void tearDown() throws Exception { + mocks.close(); + } + + // ===== READ ===== + + @Test + void read_PartitionedResource_PreservesPartitionAndTenant() { + when(daoRegistry.getResourceDao(Patient.class)).thenReturn(patientDao); + when(patientDao.read(any(), any(RequestDetails.class))).thenReturn(new Patient()); + + var repo = createRepository(createPartitionedRequestDetails()); + repo.read(Patient.class, new IdType("Patient/123"), Map.of()); + + var captor = ArgumentCaptor.forClass(RequestDetails.class); + verify(patientDao).read(any(), captor.capture()); + assertPartitionPreserved(captor.getValue(), RestOperationTypeEnum.READ); + } + + @Test + void read_NonPartitionedResource_ClearsPartitionId() { + when(daoRegistry.getResourceDao(Library.class)).thenReturn(libraryDao); + when(libraryDao.read(any(), any(RequestDetails.class))).thenReturn(new Library()); + + var repo = createRepository(createPartitionedRequestDetails()); + repo.read(Library.class, new IdType("Library/456"), Map.of()); + + var captor = ArgumentCaptor.forClass(RequestDetails.class); + verify(libraryDao).read(any(), captor.capture()); + assertPartitionCleared(captor.getValue(), RestOperationTypeEnum.READ); + } + + @Test + void read_PassesHeadersToClonedRequestDetails() { + when(daoRegistry.getResourceDao(Patient.class)).thenReturn(patientDao); + when(patientDao.read(any(), any(RequestDetails.class))).thenReturn(new Patient()); + + var repo = createRepository(createPartitionedRequestDetails()); + repo.read(Patient.class, new IdType("Patient/123"), Map.of("X-Custom", "header-value")); + + var captor = ArgumentCaptor.forClass(RequestDetails.class); + verify(patientDao).read(any(), captor.capture()); + assertEquals("header-value", captor.getValue().getHeader("X-Custom")); + } + + @Test + void read_WithoutPartitionInOriginalRequestDetails_DoesNotSetPartition() { + when(daoRegistry.getResourceDao(Patient.class)).thenReturn(patientDao); + when(patientDao.read(any(), any(RequestDetails.class))).thenReturn(new Patient()); + + var repo = createRepository(new SystemRequestDetails()); + repo.read(Patient.class, new IdType("Patient/123"), Map.of()); + + var captor = ArgumentCaptor.forClass(RequestDetails.class); + verify(patientDao).read(any(), captor.capture()); + assertInstanceOf(SystemRequestDetails.class, captor.getValue()); + assertNull(((SystemRequestDetails) captor.getValue()).getRequestPartitionId()); + } + + // ===== CREATE ===== + + @Test + void create_PartitionedResource_PreservesPartitionAndTenant() { + doReturn(patientDao).when(daoRegistry).getResourceDao(any(Patient.class)); + when(patientDao.create(any(Patient.class), any(RequestDetails.class))).thenReturn(new DaoMethodOutcome()); + + var repo = createRepository(createPartitionedRequestDetails()); + repo.create(new Patient(), Map.of()); + + var captor = ArgumentCaptor.forClass(RequestDetails.class); + verify(patientDao).create(any(Patient.class), captor.capture()); + assertPartitionPreserved(captor.getValue(), RestOperationTypeEnum.CREATE); + } + + @Test + void create_NonPartitionedResource_ClearsPartitionId() { + doReturn(libraryDao).when(daoRegistry).getResourceDao(any(Library.class)); + when(libraryDao.create(any(Library.class), any(RequestDetails.class))).thenReturn(new DaoMethodOutcome()); + + var repo = createRepository(createPartitionedRequestDetails()); + repo.create(new Library(), Map.of()); + + var captor = ArgumentCaptor.forClass(RequestDetails.class); + verify(libraryDao).create(any(Library.class), captor.capture()); + assertPartitionCleared(captor.getValue(), RestOperationTypeEnum.CREATE); + } + + // ===== UPDATE ===== + + @Test + void update_PartitionedResource_PreservesPartitionAndTenant_WhenCreated() { + doReturn(patientDao).when(daoRegistry).getResourceDao(any(Patient.class)); + var outcome = new DaoMethodOutcome(); + outcome.setCreated(true); + when(patientDao.update(any(Patient.class), any(RequestDetails.class))).thenReturn(outcome); + + var repo = createRepository(createPartitionedRequestDetails()); + var result = repo.update(new Patient(), Map.of()); + + var captor = ArgumentCaptor.forClass(RequestDetails.class); + verify(patientDao).update(any(Patient.class), captor.capture()); + assertPartitionPreserved(captor.getValue(), RestOperationTypeEnum.UPDATE); + assertEquals(201, result.getResponseStatusCode()); + } + + @Test + void update_SetsStatus200_WhenNotCreated() { + doReturn(patientDao).when(daoRegistry).getResourceDao(any(Patient.class)); + var outcome = new DaoMethodOutcome(); + outcome.setCreated(false); + when(patientDao.update(any(Patient.class), any(RequestDetails.class))).thenReturn(outcome); + + var repo = createRepository(createPartitionedRequestDetails()); + var result = repo.update(new Patient(), Map.of()); + + assertEquals(200, result.getResponseStatusCode()); + } + + @Test + void update_SetsStatus200_WhenCreatedIsNull() { + doReturn(patientDao).when(daoRegistry).getResourceDao(any(Patient.class)); + when(patientDao.update(any(Patient.class), any(RequestDetails.class))).thenReturn(new DaoMethodOutcome()); + + var repo = createRepository(createPartitionedRequestDetails()); + var result = repo.update(new Patient(), Map.of()); + + assertEquals(200, result.getResponseStatusCode()); + } + + @Test + void update_NonPartitionedResource_ClearsPartitionId() { + doReturn(libraryDao).when(daoRegistry).getResourceDao(any(Library.class)); + var outcome = new DaoMethodOutcome(); + outcome.setCreated(false); + when(libraryDao.update(any(Library.class), any(RequestDetails.class))).thenReturn(outcome); + + var repo = createRepository(createPartitionedRequestDetails()); + repo.update(new Library(), Map.of()); + + var captor = ArgumentCaptor.forClass(RequestDetails.class); + verify(libraryDao).update(any(Library.class), captor.capture()); + assertPartitionCleared(captor.getValue(), RestOperationTypeEnum.UPDATE); + } + + // ===== DELETE ===== + + @Test + void delete_PartitionedResource_PreservesPartitionAndTenant() { + when(daoRegistry.getResourceDao(Patient.class)).thenReturn(patientDao); + when(patientDao.delete(any(), any(RequestDetails.class))).thenReturn(new DaoMethodOutcome()); + + var repo = createRepository(createPartitionedRequestDetails()); + repo.delete(Patient.class, new IdType("Patient/123"), Map.of()); + + var captor = ArgumentCaptor.forClass(RequestDetails.class); + verify(patientDao).delete(any(), captor.capture()); + assertPartitionPreserved(captor.getValue(), RestOperationTypeEnum.DELETE); + } + + @Test + void delete_NonPartitionedResource_ClearsPartitionId() { + when(daoRegistry.getResourceDao(Library.class)).thenReturn(libraryDao); + when(libraryDao.delete(any(), any(RequestDetails.class))).thenReturn(new DaoMethodOutcome()); + + var repo = createRepository(createPartitionedRequestDetails()); + repo.delete(Library.class, new IdType("Library/456"), Map.of()); + + var captor = ArgumentCaptor.forClass(RequestDetails.class); + verify(libraryDao).delete(any(), captor.capture()); + assertPartitionCleared(captor.getValue(), RestOperationTypeEnum.DELETE); + } + + // ===== PATCH ===== + + @Test + void patch_PreservesPartition_BecauseParametersIsPartitionable() { + // patch uses getResourceDao(String) for DAO lookup but cloneWithAction uses + // patchParameters.getClass() (Parameters) for the partition check. + // Parameters is not in NON_PARTITIONABLE_RESOURCE_TYPES, so partition is preserved. + doReturn(patientDao).when(daoRegistry).getResourceDao("Patient"); + when(patientDao.patch(any(), any(), any(), any(), any(), any(RequestDetails.class))) + .thenReturn(new DaoMethodOutcome()); + + var repo = createRepository(createPartitionedRequestDetails()); + repo.patch(new IdType("Patient/123"), new Parameters(), Map.of()); + + var captor = ArgumentCaptor.forClass(RequestDetails.class); + verify(patientDao).patch(any(), isNull(), eq(PatchTypeEnum.FHIR_PATCH_JSON), isNull(), any(), captor.capture()); + assertPartitionPreserved(captor.getValue(), RestOperationTypeEnum.PATCH); + } + + // ===== Helpers ===== + + private SystemRequestDetails createPartitionedRequestDetails() { + var details = new SystemRequestDetails(); + details.setRequestPartitionId(PARTITION_ID); + details.setTenantId(TENANT_ID); + return details; + } + + private ClinicalIntelligenceHapiFhirRepository createRepository(SystemRequestDetails requestDetails) { + return new ClinicalIntelligenceHapiFhirRepository(daoRegistry, requestDetails, new RestfulServer(FHIR_CONTEXT)); + } + + private void assertPartitionPreserved(RequestDetails clonedDetails, RestOperationTypeEnum expectedOp) { + assertInstanceOf(SystemRequestDetails.class, clonedDetails); + var systemDetails = (SystemRequestDetails) clonedDetails; + assertEquals(expectedOp, clonedDetails.getRestOperationType()); + assertEquals(RequestTypeEnum.POST, clonedDetails.getRequestType()); + assertEquals(TENANT_ID, clonedDetails.getTenantId()); + assertEquals(PARTITION_ID, systemDetails.getRequestPartitionId()); + } + + private void assertPartitionCleared(RequestDetails clonedDetails, RestOperationTypeEnum expectedOp) { + assertInstanceOf(SystemRequestDetails.class, clonedDetails); + var systemDetails = (SystemRequestDetails) clonedDetails; + assertEquals(expectedOp, clonedDetails.getRestOperationType()); + assertEquals(RequestTypeEnum.POST, clonedDetails.getRequestType()); + // Tenant ID is preserved because the SystemRequestDetails constructor copies it + assertEquals(TENANT_ID, clonedDetails.getTenantId()); + // Partition ID is cleared for non-partitionable resources + assertNull(systemDetails.getRequestPartitionId()); + } +} diff --git a/cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceRequestDetailsClonerTest.java b/cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceRequestDetailsClonerTest.java index 1ba699cded..1c3be3b14d 100644 --- a/cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceRequestDetailsClonerTest.java +++ b/cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/repository/ClinicalIntelligenceRequestDetailsClonerTest.java @@ -16,6 +16,7 @@ import ca.uhn.fhir.rest.api.server.SystemRestfulResponse; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import jakarta.annotation.Nonnull; import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; @@ -31,6 +32,7 @@ class ClinicalIntelligenceRequestDetailsClonerTest { record RequestDetailsVariant(String label, RequestDetails requestDetails) { @Override + @Nonnull public String toString() { return label; } @@ -175,8 +177,7 @@ static Stream partitionableResourceVariants() { "R4 Patient", FhirContext.forR4Cached(), org.hl7.fhir.r4.model.Patient.class, true), new PartitionableResourceVariant( "R4 Observation", FhirContext.forR4Cached(), org.hl7.fhir.r4.model.Observation.class, true), - new PartitionableResourceVariant( - "R4 Measure", FhirContext.forR4Cached(), org.hl7.fhir.r4.model.Measure.class, true), + new PartitionableResourceVariant("R4 Measure", FhirContext.forR4Cached(), Measure.class, true), new PartitionableResourceVariant( "R4 Encounter", FhirContext.forR4Cached(), org.hl7.fhir.r4.model.Encounter.class, true), // R4 non-partitionable resources (all 11 types)