From fe1fc3a928e532cc990cb6efeeb9463081baff10 Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Sun, 17 Nov 2024 16:49:28 -0500 Subject: [PATCH] Test cleanup --- .../ca/uhn/fhir/model/api/IFhirVersion.java | 9 + .../MdmReadVirtualizationInterceptorTest.java | 177 +++++++++--- .../uhn/fhir/mdm/api/IMdmLinkExpandSvc.java | 3 +- .../ca/uhn/fhir/mdm/api/MdmConstants.java | 13 + .../MdmReadVirtualizationInterceptor.java | 269 +++++++----------- .../MdmSearchExpandingInterceptor.java | 19 +- .../ca/uhn/fhir/mdm/svc/MdmLinkExpandSvc.java | 3 +- .../mdm/svc/MdmSearchExpansionResults.java | 68 +++++ .../fhir/mdm/svc/MdmSearchExpansionSvc.java | 158 +++++++--- 9 files changed, 468 insertions(+), 251 deletions(-) create mode 100644 hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSearchExpansionResults.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IFhirVersion.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IFhirVersion.java index 325bb3a7d90..f8c737981ce 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IFhirVersion.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IFhirVersion.java @@ -63,6 +63,15 @@ public interface IFhirVersion { IIdType newIdType(); + /** + * @since 8.0.0 + */ + default IIdType newIdType(String theValue) { + IIdType retVal = newIdType(); + retVal.setValue(theValue); + return retVal; + } + /** * Returns an instance of IFhirVersionServer for this version. * Note that this method may only be called if the hapi-fhir-server diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmReadVirtualizationInterceptorTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmReadVirtualizationInterceptorTest.java index 8aef0f99f1e..ab04f341351 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmReadVirtualizationInterceptorTest.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmReadVirtualizationInterceptorTest.java @@ -11,15 +11,21 @@ import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.mdm.interceptor.MdmReadVirtualizationInterceptor; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.param.TokenAndListParam; import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; import jakarta.annotation.Nonnull; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Reference; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -31,10 +37,12 @@ import org.springframework.test.context.ContextConfiguration; import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.TreeSet; import java.util.UUID; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.when; @ContextConfiguration(classes = {MdmHelperConfig.class}) @@ -64,7 +72,6 @@ public class MdmReadVirtualizationInterceptorTest extends BaseMdmR4Test { @BeforeEach public void before() throws Exception { super.before(); - when(mySrd.getRestOperationType()).thenReturn(RestOperationTypeEnum.SEARCH_TYPE); } @Override @@ -83,14 +90,15 @@ public class MdmReadVirtualizationInterceptorTest extends BaseMdmR4Test { @ValueSource(booleans = {true, false}) public void testRead_ObservationReferencingSourcePatient(boolean theUseClientAssignedIds) { // Setup - createTestData(theUseClientAssignedIds); + createTestPatientsAndObservations(theUseClientAssignedIds); registerVirtualizationInterceptor(); + when(mySrd.getRestOperationType()).thenReturn(RestOperationTypeEnum.READ); // Test Observation obs = myObservationDao.read(myObservationReferencingSourcePatientA0Id, mySrd); // Verify - assertEquals(myGoldenResourcePatientAId.getValue(), obs.getSubject().getReference()); + assertEquals(mySourcePatientA0Id.getValue(), obs.getSubject().getReference()); } /** @@ -101,8 +109,9 @@ public class MdmReadVirtualizationInterceptorTest extends BaseMdmR4Test { @ValueSource(booleans = {true, false}) public void testRead_ObservationReferencingGoldenPatient(boolean theUseClientAssignedIds) { // Setup - createTestData(theUseClientAssignedIds); + createTestPatientsAndObservations(theUseClientAssignedIds); registerVirtualizationInterceptor(); + when(mySrd.getRestOperationType()).thenReturn(RestOperationTypeEnum.READ); // Test Observation obs = myObservationDao.read(myObservationReferencingGoldenPatientAId, mySrd); @@ -117,15 +126,22 @@ public class MdmReadVirtualizationInterceptorTest extends BaseMdmR4Test { @Test public void testSearch_Patient_FetchAll() { // Setup - createTestData(false); + createTestPatientsAndObservations(false); registerVirtualizationInterceptor(); + when(mySrd.getRestOperationType()).thenReturn(RestOperationTypeEnum.SEARCH_TYPE); // Test IBundleProvider outcome = myPatientDao.search(SearchParameterMap.newSynchronous(), mySrd); // Verify List ids = toUnqualifiedVersionlessIdValues(outcome); - assertThat(ids).asList().containsExactlyInAnyOrder(myGoldenResourcePatientAId.getValue(), myGoldenResourcePatientBId.getValue()); + assertThat(ids).asList().containsExactlyInAnyOrder( + mySourcePatientA0Id.getValue(), + mySourcePatientA1Id.getValue(), + mySourcePatientA2Id.getValue(), + mySourcePatientB0Id.getValue(), + myGoldenResourcePatientAId.getValue(), + myGoldenResourcePatientBId.getValue()); } /** @@ -135,8 +151,9 @@ public class MdmReadVirtualizationInterceptorTest extends BaseMdmR4Test { @Test public void testSearch_Patient_FetchOnlySource() { // Setup - createTestData(false); + createTestPatientsAndObservations(false); registerVirtualizationInterceptor(); + when(mySrd.getRestOperationType()).thenReturn(RestOperationTypeEnum.SEARCH_TYPE); // Test SearchParameterMap params = SearchParameterMap.newSynchronous(); @@ -147,7 +164,7 @@ public class MdmReadVirtualizationInterceptorTest extends BaseMdmR4Test { // Verify List ids = toUnqualifiedVersionlessIdValues(outcome); - assertThat(ids).asList().containsExactlyInAnyOrder(myGoldenResourcePatientAId.getValue(), myGoldenResourcePatientBId.getValue()); + assertThat(ids).asList().containsExactlyInAnyOrder(mySourcePatientA0Id.getValue(), mySourcePatientB0Id.getValue()); } /** @@ -156,40 +173,40 @@ public class MdmReadVirtualizationInterceptorTest extends BaseMdmR4Test { */ @ParameterizedTest @ValueSource(booleans = {true, false}) - public void testSearch_Patient_FetchAll_AlsoRevIncludeDependentResources(boolean theUseClientAssginedId) { + public void testSearch_Patient_FetchSourcePatient_AlsoRevIncludeDependentResources(boolean theUseClientAssginedId) { // Setup - createTestData(theUseClientAssginedId); + createTestPatientsAndObservations(theUseClientAssginedId); registerVirtualizationInterceptor(); + when(mySrd.getRestOperationType()).thenReturn(RestOperationTypeEnum.SEARCH_TYPE); // Test - SearchParameterMap params = new SearchParameterMap(); + SearchParameterMap params = SearchParameterMap.newSynchronous(); + params.add(Observation.SP_RES_ID, new TokenParam(mySourcePatientA2Id.getValue())); params.addRevInclude(IBaseResource.INCLUDE_ALL); IBundleProvider outcome = myPatientDao.search(params, mySrd); // Verify List ids = toUnqualifiedVersionlessIdValues(outcome); assertThat(ids).asList().containsExactlyInAnyOrder( - myGoldenResourcePatientAId.getValue(), - myGoldenResourcePatientBId.getValue(), + mySourcePatientA2Id.getValue(), myObservationReferencingGoldenPatientAId.getValue(), myObservationReferencingSourcePatientA0Id.getValue(), myObservationReferencingSourcePatientA1Id.getValue(), - myObservationReferencingSourcePatientA2Id.getValue(), - myObservationReferencingSourcePatientB0Id.getValue() + myObservationReferencingSourcePatientA2Id.getValue() ); Map resources = toResourceIdValueMap(outcome); - Observation obs; - obs = (Observation) resources.get(myObservationReferencingGoldenPatientAId.getValue()); - assertEquals(myGoldenResourcePatientAId.getValue(), obs.getSubject().getReference()); - obs = (Observation) resources.get(myObservationReferencingSourcePatientB0Id.getValue()); - assertEquals(myGoldenResourcePatientBId.getValue(), obs.getSubject().getReference()); + assertEquals(mySourcePatientA2Id.getValue(), getObservation(resources, myObservationReferencingGoldenPatientAId).getSubject().getReference()); + assertEquals(mySourcePatientA2Id.getValue(), getObservation(resources, myObservationReferencingSourcePatientA0Id).getSubject().getReference()); + assertEquals(mySourcePatientA2Id.getValue(), getObservation(resources, myObservationReferencingSourcePatientA1Id).getSubject().getReference()); + assertEquals(mySourcePatientA2Id.getValue(), getObservation(resources, myObservationReferencingSourcePatientA2Id).getSubject().getReference()); } @Test public void testSearch_Observation_SpecificSourcePatient() { // Setup - createTestData(false); + createTestPatientsAndObservations(true); registerVirtualizationInterceptor(); + when(mySrd.getRestOperationType()).thenReturn(RestOperationTypeEnum.SEARCH_TYPE); // Test SearchParameterMap params = SearchParameterMap.newSynchronous(); @@ -200,22 +217,123 @@ public class MdmReadVirtualizationInterceptorTest extends BaseMdmR4Test { // Verify List ids = toUnqualifiedVersionlessIdValues(outcome); assertThat(ids).asList().containsExactlyInAnyOrder( - myGoldenResourcePatientAId.getValue(), + mySourcePatientA2Id.getValue(), myObservationReferencingGoldenPatientAId.getValue(), myObservationReferencingSourcePatientA0Id.getValue(), myObservationReferencingSourcePatientA1Id.getValue(), myObservationReferencingSourcePatientA2Id.getValue() ); Map resources = toResourceIdValueMap(outcome); - Observation obs = (Observation) resources.get(myObservationReferencingGoldenPatientAId.getValue()); - assertEquals(myGoldenResourcePatientAId.getValue(), obs.getSubject().getReference()); + assertEquals(mySourcePatientA2Id.getValue(), getObservation(resources, myObservationReferencingGoldenPatientAId).getSubject().getReference()); + assertEquals(mySourcePatientA2Id.getValue(), getObservation(resources, myObservationReferencingSourcePatientA0Id).getSubject().getReference()); + assertEquals(mySourcePatientA2Id.getValue(), getObservation(resources, myObservationReferencingSourcePatientA1Id).getSubject().getReference()); + assertEquals(mySourcePatientA2Id.getValue(), getObservation(resources, myObservationReferencingSourcePatientA2Id).getSubject().getReference()); + } + + @Test + public void testSearch_Observation_NonRelativeReferencesAreLeftAlone() { + // Setup + createTestPatients(true); + registerVirtualizationInterceptor(); + when(mySrd.getRestOperationType()).thenReturn(RestOperationTypeEnum.SEARCH_TYPE); + + IIdType obsId = createObservation(withSubject(mySourcePatientA0Id), withObservationCode("http://foo", "code0")).toUnqualifiedVersionless(); + Observation obs = myObservationDao.read(obsId, mySrd); + Encounter encounter = new Encounter(); + encounter.setId("1"); + encounter.setStatus(Encounter.EncounterStatus.ARRIVED); + obs.getContained().add(encounter); + + // Add 2 non-relative references. The interceptor should just ignore these + obs.setEncounter(new Reference("#1")); + obs.addBasedOn().setIdentifier(new Identifier().setValue("123")); + myObservationDao.update(obs, mySrd); + + logAllResourceLinks(); + + // Test + SearchParameterMap params = SearchParameterMap.newSynchronous(); + params.add(Observation.SP_SUBJECT, new ReferenceParam(mySourcePatientA2Id.getValue())); + params.addInclude(Observation.INCLUDE_PATIENT); + IBundleProvider outcome = myObservationDao.search(params, mySrd); + + // Verify + List ids = toUnqualifiedVersionlessIdValues(outcome); + assertThat(ids).asList().containsExactlyInAnyOrder( + mySourcePatientA2Id.getValue(), + obsId.getValue() + ); + + } + + /** + * The CQL evaluator uses a shared RequestDetails across multiple different requests - Make sure + * we don't return the wrong cached results + */ + @Test + public void testSearch_RequestDetailsIsReused() { + // Setup + createTestPatientsAndObservations(true); + registerVirtualizationInterceptor(); + when(mySrd.getRestOperationType()).thenReturn(RestOperationTypeEnum.SEARCH_TYPE); + + // Test + SystemRequestDetails requestDetails = new SystemRequestDetails(); + + // Search for patients + requestDetails.setResourceName("Patient"); + SearchParameterMap params = SearchParameterMap.newSynchronous(); + params.add(IAnyResource.SP_RES_ID, new TokenParam(mySourcePatientA2Id.getValue())); + IBundleProvider outcome = myPatientDao.search(params, mySrd); + List ids = toUnqualifiedVersionlessIdValues(outcome); + assertThat(ids).asList().containsExactlyInAnyOrder( + mySourcePatientA2Id.getValue() + ); + + // Search for Observations + requestDetails.setResourceName("Observation"); + params = SearchParameterMap.newSynchronous(); + params.add(Observation.SP_SUBJECT, new ReferenceParam(mySourcePatientA2Id.getValue())); + params.addInclude(Observation.INCLUDE_PATIENT); + outcome = myObservationDao.search(params, mySrd); + + // Verify + ids = toUnqualifiedVersionlessIdValues(outcome); + assertThat(ids).asList().containsExactlyInAnyOrder( + mySourcePatientA2Id.getValue(), + myObservationReferencingGoldenPatientAId.getValue(), + myObservationReferencingSourcePatientA0Id.getValue(), + myObservationReferencingSourcePatientA1Id.getValue(), + myObservationReferencingSourcePatientA2Id.getValue() + ); + + } + + private static Observation getObservation(Map resources, IIdType observationReferencingGoldenPatientAId) { + Observation retVal = (Observation) resources.get(observationReferencingGoldenPatientAId.getValue()); + if (retVal == null) { + fail("Could not find '" + observationReferencingGoldenPatientAId.getValue() + "' - Valid IDs: " + new TreeSet<>(resources.keySet())); + } + return retVal; } private void registerVirtualizationInterceptor() { myInterceptorRegistry.registerInterceptor(myInterceptor); } - private void createTestData(boolean theUseClientAssignedIds) { + private void createTestPatientsAndObservations(boolean theUseClientAssignedIds) { + createTestPatients(theUseClientAssignedIds); + + myObservationReferencingSourcePatientA0Id = createObservation(theUseClientAssignedIds, mySourcePatientA0Id, "code0"); + myObservationReferencingSourcePatientA1Id = createObservation(theUseClientAssignedIds, mySourcePatientA1Id, "code1"); + myObservationReferencingSourcePatientA2Id = createObservation(theUseClientAssignedIds, mySourcePatientA2Id, "code2"); + myObservationReferencingGoldenPatientAId = createObservation(theUseClientAssignedIds, myGoldenResourcePatientAId, "code2"); + myObservationReferencingSourcePatientB0Id = createObservation(theUseClientAssignedIds, mySourcePatientB0Id, "code0"); + + logAllResources(); + } + + private void createTestPatients(boolean theUseClientAssignedIds) { String inputState; if (theUseClientAssignedIds) { inputState = """ @@ -243,17 +361,8 @@ public class MdmReadVirtualizationInterceptorTest extends BaseMdmR4Test { mySourcePatientB0Id = toId(state, outcome.getResults().get(3).getSourcePersistenceId()); myGoldenResourcePatientBId = toId(state, outcome.getResults().get(3).getGoldenResourcePersistenceId()); assertEquals(4, logAllMdmLinks()); - - myObservationReferencingSourcePatientA0Id = createObservation(theUseClientAssignedIds, mySourcePatientA0Id, "code0"); - myObservationReferencingSourcePatientA1Id = createObservation(theUseClientAssignedIds, mySourcePatientA1Id, "code1"); - myObservationReferencingSourcePatientA2Id = createObservation(theUseClientAssignedIds, mySourcePatientA2Id, "code2"); - myObservationReferencingGoldenPatientAId = createObservation(theUseClientAssignedIds, myGoldenResourcePatientAId, "code2"); - myObservationReferencingSourcePatientB0Id = createObservation(theUseClientAssignedIds, mySourcePatientB0Id, "code0"); - assertEquals(!theUseClientAssignedIds, mySourcePatientA0Id.isIdPartValidLong()); assertEquals(!theUseClientAssignedIds, myGoldenResourcePatientAId.isIdPartValidLong()); - - logAllResources(); } @Nonnull diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmLinkExpandSvc.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmLinkExpandSvc.java index d27f8b0cb77..2a886ea1ad7 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmLinkExpandSvc.java +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmLinkExpandSvc.java @@ -20,7 +20,6 @@ package ca.uhn.fhir.mdm.api; import ca.uhn.fhir.interceptor.model.RequestPartitionId; -import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -41,5 +40,5 @@ public interface IMdmLinkExpandSvc { Set expandMdmByGoldenResourcePid( RequestPartitionId theRequestPartitionId, IResourcePersistentId theGoldenResourcePid); - Set expandMdmByGoldenResourceId(RequestPartitionId theRequestPartitionId, IdDt theId); + Set expandMdmByGoldenResourceId(RequestPartitionId theRequestPartitionId, IIdType theId); } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MdmConstants.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MdmConstants.java index 4ea9ff433be..7bca272ec1d 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MdmConstants.java +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MdmConstants.java @@ -53,4 +53,17 @@ public class MdmConstants { "This resource was found to be a duplicate and has been redirected."; public static final String UNKNOWN_MDM_TYPES = "Unknown Resource Types"; + + /** + * Interceptor order constant for {@link ca.uhn.fhir.mdm.interceptor.MdmReadVirtualizationInterceptor}, which + * should fire before {@link ca.uhn.fhir.mdm.interceptor.MdmSearchExpandingInterceptor} since it is a + * superset of the same functionality and only one should run if they are both registered for whatever + * reason. + */ + public static final int ORDER_PRESEARCH_REGISTERED_MDM_READ_VIRTUALIZATION_INTERCEPTOR = 0; + + /** + * @see #ORDER_PRESEARCH_REGISTERED_MDM_READ_VIRTUALIZATION_INTERCEPTOR + */ + public static final int ORDER_PRESEARCH_REGISTERED_MDM_SEARCH_EXPANDING_INTERCEPTOR = 1; } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/interceptor/MdmReadVirtualizationInterceptor.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/interceptor/MdmReadVirtualizationInterceptor.java index d1ad4184611..22d88fbdfc8 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/interceptor/MdmReadVirtualizationInterceptor.java +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/interceptor/MdmReadVirtualizationInterceptor.java @@ -22,34 +22,32 @@ package ca.uhn.fhir.mdm.interceptor; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.interceptor.api.Hook; import ca.uhn.fhir.interceptor.api.Pointcut; -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.PersistentIdToForcedIdMap; import ca.uhn.fhir.jpa.api.svc.IIdHelperService; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.mdm.api.MdmConstants; import ca.uhn.fhir.mdm.dao.IMdmLinkDao; -import ca.uhn.fhir.mdm.model.MdmPidTuple; +import ca.uhn.fhir.mdm.svc.MdmSearchExpansionResults; import ca.uhn.fhir.mdm.svc.MdmSearchExpansionSvc; import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.ResourceReferenceInfo; -import com.google.common.collect.ListMultimap; -import com.google.common.collect.MultimapBuilder; -import jakarta.annotation.Nonnull; -import org.hl7.fhir.instance.model.api.IBaseReference; +import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; /** * This class is experimental and subject to change. Use with caution. @@ -75,9 +73,13 @@ import java.util.Set; * @since 8.0.0 */ public class MdmReadVirtualizationInterceptor

> { + private static final Logger ourLog = LoggerFactory.getLogger(MdmReadVirtualizationInterceptor.class); - public static final String CURRENTLY_PROCESSING_FLAG = - MdmReadVirtualizationInterceptor.class.getName() + "-CURRENTLY-PROCESSING"; + private static final String CURRENTLY_PROCESSING_FLAG = + MdmReadVirtualizationInterceptor.class.getName() + "_CURRENTLY_PROCESSING"; + private static final MdmSearchExpansionSvc.IParamTester PARAM_TESTER_NO_RES_ID = + (paramName, param) -> !IAnyResource.SP_RES_ID.equals(paramName); + private static final MdmSearchExpansionSvc.IParamTester PARAM_TESTER_ALL = (paramName, param) -> true; @Autowired private FhirContext myFhirContext; @@ -97,191 +99,118 @@ public class MdmReadVirtualizationInterceptor

@Autowired private HapiTransactionService myTxService; - @Hook(Pointcut.STORAGE_PRESEARCH_REGISTERED) + @Hook( + value = Pointcut.STORAGE_PRESEARCH_REGISTERED, + order = MdmConstants.ORDER_PRESEARCH_REGISTERED_MDM_READ_VIRTUALIZATION_INTERCEPTOR) public void hook(RequestDetails theRequestDetails, SearchParameterMap theSearchParameterMap) { - myMdmSearchExpansionSvc.expandSearch(theRequestDetails, theSearchParameterMap, t -> true); + ourLog.atDebug() + .setMessage("Original search: {}{}") + .addArgument(theRequestDetails.getResourceName()) + .addArgument(() -> theSearchParameterMap.toNormalizedQueryString(myFhirContext)) + .log(); + + if (theSearchParameterMap.hasIncludes() || theSearchParameterMap.hasRevIncludes()) { + myMdmSearchExpansionSvc.expandSearchAndStoreInRequestDetails( + theRequestDetails, theSearchParameterMap, PARAM_TESTER_ALL); + } else { + // If we don't have any includes, it's not worth auto-expanding the _id parameter since we'll only end + // up filtering out the extra resources afterward + myMdmSearchExpansionSvc.expandSearchAndStoreInRequestDetails( + theRequestDetails, theSearchParameterMap, PARAM_TESTER_NO_RES_ID); + } + + ourLog.atDebug() + .setMessage("R search: {}{}") + .addArgument(theRequestDetails.getResourceName()) + .addArgument(() -> theSearchParameterMap.toNormalizedQueryString(myFhirContext)) + .log(); } @SuppressWarnings("EnumSwitchStatementWhichMissesCases") @Hook(Pointcut.STORAGE_PRESHOW_RESOURCES) public void preShowResources(RequestDetails theRequestDetails, IPreResourceShowDetails theDetails) { - if (theRequestDetails.getUserData().get(CURRENTLY_PROCESSING_FLAG) == Boolean.TRUE) { + MdmSearchExpansionResults expansionResults = MdmSearchExpansionSvc.getCachedExpansionResults(theRequestDetails); + if (expansionResults == null) { + // This means the PRESEARCH hook didn't save anything, which probably means + // no RequestDetails is available return; } - switch (theRequestDetails.getRestOperationType()) { - case SEARCH_TYPE: - case SEARCH_SYSTEM: - case GET_PAGE: - break; - default: - return; + + if (theRequestDetails.getUserData().get(CURRENTLY_PROCESSING_FLAG) != null) { + // Avoid recursive calls + return; } - // Gather all the resource IDs we might need to remap - ListMultimap candidateResourceIds = extractRemapCandidateResources(theDetails); - ListMultimap candidateReferences = extractRemapCandidateReferences(theDetails); - - CandidateMdmLinkedResources

candidates = - findCandidateMdmLinkedResources(candidateResourceIds, candidateReferences); - - // Loop through each link and figure out whether we need to remap anything - for (MdmPidTuple

tuple : candidates.getTuples()) { - Optional sourceIdOpt = candidates.getFhirIdForPersistentId(tuple.getSourcePid()); - if (sourceIdOpt.isPresent()) { - String sourceId = sourceIdOpt.get(); - - // Remap references from source to golden - List referencesToRemap = candidateReferences.get(sourceId); - if (!referencesToRemap.isEmpty()) { - P associatedGoldenResourcePid = tuple.getGoldenPid(); - Optional associatedGoldenResourceId = - candidates.getFhirIdForPersistentId(associatedGoldenResourcePid); - if (associatedGoldenResourceId.isPresent()) { - for (ResourceReferenceInfo referenceInfoToRemap : referencesToRemap) { - IBaseReference referenceToRemap = referenceInfoToRemap.getResourceReference(); - referenceToRemap.setReference(associatedGoldenResourceId.get()); - } - } - } - - // Filter out source resources - Optional targetIdOpt = candidates.getFhirIdForPersistentId(tuple.getGoldenPid()); - if (targetIdOpt.isPresent()) { - Integer filteredIndex = null; - for (int sourceIdResourceIndex : candidateResourceIds.get(sourceId)) { - theDetails.setResource(sourceIdResourceIndex, null); - if (filteredIndex == null) { - filteredIndex = sourceIdResourceIndex; - } - } - - if (filteredIndex != null) { - String targetId = targetIdOpt.get(); - if (candidateResourceIds.get(targetId).isEmpty()) { - // If we filtered a resource out because it's not a golden record, - // and the golden record itself isn't already a part of the results, - // then we'll manually add it - IIdType targetResourceId = newIdType(targetId); - IFhirResourceDao dao = myDaoRegistry.getResourceDao(targetResourceId.getResourceType()); - - theRequestDetails.getUserData().put(CURRENTLY_PROCESSING_FLAG, Boolean.TRUE); - IBaseResource goldenResource; - try { - goldenResource = dao.read(targetResourceId, theRequestDetails); - } finally { - theRequestDetails.getUserData().remove(CURRENTLY_PROCESSING_FLAG); - } - theDetails.setResource(filteredIndex, goldenResource); - candidateResourceIds.put(targetId, filteredIndex); - } - } - } - } - } - } - - @Nonnull - private CandidateMdmLinkedResources

findCandidateMdmLinkedResources( - ListMultimap candidateResourceIds, - ListMultimap candidateReferences) { - return myTxService.withSystemRequest().read(() -> { - // Resolve all the resource IDs we've seen that could be MDM candidates, - // and look for MDM links that have these IDs as either the source or the - // golden resource side of the link - Set allIds = new HashSet<>(); - candidateResourceIds.keySet().forEach(t -> allIds.add(newIdType(t))); - candidateReferences.keySet().forEach(t -> allIds.add(newIdType(t))); - List

sourcePids = - myIdHelperService.getPidsOrThrowException(RequestPartitionId.allPartitions(), List.copyOf(allIds)); - Collection> tuples = myMdmLinkDao.resolveGoldenResources(sourcePids); - - // Resolve the link PIDs into FHIR IDs - Set

allPersistentIds = new HashSet<>(); - tuples.forEach(t -> allPersistentIds.add(t.getGoldenPid())); - tuples.forEach(t -> allPersistentIds.add(t.getSourcePid())); - PersistentIdToForcedIdMap

persistentIdToFhirIdMap = - myIdHelperService.translatePidsToForcedIds(allPersistentIds); - return new CandidateMdmLinkedResources<>(tuples, persistentIdToFhirIdMap); - }); - } - - /** - * @return Returns a map where the keys are a typed ID (Patient/ABC) and the values are the index of - * that resource within the {@link IPreResourceShowDetails} - */ - private ListMultimap extractRemapCandidateResources(IPreResourceShowDetails theDetails) { - ListMultimap retVal = - MultimapBuilder.hashKeys().arrayListValues().build(); + /* + * If a resource being returned is a resource that was mdm-expanded, + * we'll replace that resource with the originally requested resource, + * making sure to avoid adding duplicates to the results. + */ + Set resourcesInBundle = new HashSet<>(); for (int resourceIdx = 0; resourceIdx < theDetails.size(); resourceIdx++) { IBaseResource resource = theDetails.getResource(resourceIdx); - - // Extract the IDs of the actual resources being returned in case - // we want to replace them with golden equivalents - if (isRemapCandidate(resource.getIdElement().getResourceType())) { - IIdType id = resource.getIdElement().toUnqualifiedVersionless(); - retVal.put(id.getValue(), resourceIdx); + IIdType id = resource.getIdElement().toUnqualifiedVersionless(); + Optional originalIdOpt = expansionResults.getOriginalIdForExpandedId(id); + if (originalIdOpt.isPresent()) { + IIdType originalId = originalIdOpt.get(); + if (resourcesInBundle.add(originalId)) { + IBaseResource originalResource = fetchResourceFromRepository(theRequestDetails, originalId); + theDetails.setResource(resourceIdx, originalResource); + } else { + theDetails.setResource(resourceIdx, null); + } + } else { + if (!resourcesInBundle.add(id)) { + theDetails.setResource(resourceIdx, null); + } } } - return retVal; - } - - /** - * @return Returns a map where the keys are a typed ID (Patient/ABC) and the values are references - * found in any of the resources that are referring to that ID. - */ - private ListMultimap extractRemapCandidateReferences( - IPreResourceShowDetails theDetails) { - ListMultimap retVal = - MultimapBuilder.hashKeys().arrayListValues().build(); FhirTerser terser = myFhirContext.newTerser(); - for (int resourceIdx = 0; resourceIdx < theDetails.size(); resourceIdx++) { IBaseResource resource = theDetails.getResource(resourceIdx); + if (resource != null) { - // Extract all the references in the resources we're returning - // in case we need to remap them to golden equivalents - List referenceInfos = terser.getAllResourceReferences(resource); - for (ResourceReferenceInfo referenceInfo : referenceInfos) { - IIdType referenceId = referenceInfo.getResourceReference().getReferenceElement(); - - if (isRemapCandidate(referenceId.getResourceType())) { - IIdType id = referenceId.toUnqualifiedVersionless(); - retVal.put(id.getValue(), referenceInfo); + // Extract all the references in the resources we're returning + // in case we need to remap them to golden equivalents + List referenceInfos = terser.getAllResourceReferences(resource); + for (ResourceReferenceInfo referenceInfo : referenceInfos) { + IIdType referenceId = referenceInfo + .getResourceReference() + .getReferenceElement() + .toUnqualifiedVersionless(); + if (referenceId.hasResourceType() + && referenceId.hasIdPart() + && !referenceId.isLocal() + && !referenceId.isUuid()) { + Optional nonExpandedId = expansionResults.getOriginalIdForExpandedId(referenceId); + if (nonExpandedId.isPresent()) { + referenceInfo + .getResourceReference() + .setReference(nonExpandedId.get().getValue()); + } + } } } } - return retVal; + ourLog.atDebug().setMessage("Returning resources: {}").addArgument(() -> theDetails.getAllResources().stream() + .map(t -> t.getIdElement().toUnqualifiedVersionless().getValue()) + .sorted() + .collect(Collectors.toList())).log(); + } - private IIdType newIdType(String targetId) { - return myFhirContext.getVersion().newIdType().setValue(targetId); - } - - /** - * Is the given resource a candidate for virtualization? - */ - private boolean isRemapCandidate(String theResourceType) { - return "Patient".equals(theResourceType); - } - - private static class CandidateMdmLinkedResources

> { - private final Collection> myTuples; - private final PersistentIdToForcedIdMap

myPersistentIdToFhirIdMap; - - public CandidateMdmLinkedResources( - Collection> thePidTuples, PersistentIdToForcedIdMap

thePersistentIdToForcedIdMap) { - this.myTuples = thePidTuples; - this.myPersistentIdToFhirIdMap = thePersistentIdToForcedIdMap; - } - - public Collection> getTuples() { - return myTuples; - } - - public Optional getFhirIdForPersistentId(P theSourcePid) { - return myPersistentIdToFhirIdMap.get(theSourcePid); + private IBaseResource fetchResourceFromRepository(RequestDetails theRequestDetails, IIdType originalId) { + IFhirResourceDao dao = myDaoRegistry.getResourceDao(originalId.getResourceType()); + theRequestDetails.getUserData().put(CURRENTLY_PROCESSING_FLAG, Boolean.TRUE); + IBaseResource originalResource; + try { + originalResource = dao.read(originalId, theRequestDetails); + } finally { + theRequestDetails.getUserData().remove(CURRENTLY_PROCESSING_FLAG); } + return originalResource; } + } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/interceptor/MdmSearchExpandingInterceptor.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/interceptor/MdmSearchExpandingInterceptor.java index e82a611afc1..c7449beffd9 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/interceptor/MdmSearchExpandingInterceptor.java +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/interceptor/MdmSearchExpandingInterceptor.java @@ -24,9 +24,11 @@ import ca.uhn.fhir.interceptor.api.Interceptor; import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.mdm.api.MdmConstants; import ca.uhn.fhir.mdm.svc.MdmSearchExpansionSvc; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.param.TokenParam; import org.springframework.beans.factory.annotation.Autowired; /** @@ -36,17 +38,30 @@ import org.springframework.beans.factory.annotation.Autowired; @Interceptor public class MdmSearchExpandingInterceptor { + private static final MdmSearchExpansionSvc.IParamTester PARAM_TESTER = (paramName, param) -> { + boolean retVal = false; + if (param instanceof ReferenceParam) { + retVal = ((ReferenceParam) param).isMdmExpand(); + } else if (param instanceof TokenParam) { + retVal = ((TokenParam) param).isMdmExpand(); + } + return retVal; + }; + @Autowired private JpaStorageSettings myStorageSettings; @Autowired private MdmSearchExpansionSvc myMdmSearchExpansionSvc; - @Hook(Pointcut.STORAGE_PRESEARCH_REGISTERED) + @Hook( + value = Pointcut.STORAGE_PRESEARCH_REGISTERED, + order = MdmConstants.ORDER_PRESEARCH_REGISTERED_MDM_SEARCH_EXPANDING_INTERCEPTOR) public void hook(RequestDetails theRequestDetails, SearchParameterMap theSearchParameterMap) { if (myStorageSettings.isAllowMdmExpansion()) { - myMdmSearchExpansionSvc.expandSearch(theRequestDetails, theSearchParameterMap, ReferenceParam::isMdmExpand); + myMdmSearchExpansionSvc.expandSearchAndStoreInRequestDetails( + theRequestDetails, theSearchParameterMap, PARAM_TESTER); } } } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmLinkExpandSvc.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmLinkExpandSvc.java index f1a1364a500..cc3ddc9fa46 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmLinkExpandSvc.java +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmLinkExpandSvc.java @@ -26,7 +26,6 @@ import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; import ca.uhn.fhir.mdm.dao.IMdmLinkDao; import ca.uhn.fhir.mdm.log.Logs; import ca.uhn.fhir.mdm.model.MdmPidTuple; -import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; import jakarta.annotation.Nonnull; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -137,7 +136,7 @@ public class MdmLinkExpandSvc implements IMdmLinkExpandSvc { } @Override - public Set expandMdmByGoldenResourceId(RequestPartitionId theRequestPartitionId, IdDt theId) { + public Set expandMdmByGoldenResourceId(RequestPartitionId theRequestPartitionId, IIdType theId) { ourLog.debug("About to expand golden resource with golden resource id {}", theId); IResourcePersistentId pidOrThrowException = myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theId); diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSearchExpansionResults.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSearchExpansionResults.java new file mode 100644 index 00000000000..44fc5d8e5cf --- /dev/null +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSearchExpansionResults.java @@ -0,0 +1,68 @@ +/*- + * #%L + * HAPI FHIR - Master Data Management + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.mdm.svc; + +import org.hl7.fhir.instance.model.api.IIdType; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * Result object for {@link MdmSearchExpansionSvc} + * + * @since 8.0.0 + */ +public class MdmSearchExpansionResults { + + private Set myOriginalIdToExpandedId = new HashSet<>(); + private Map myExpandedIdToOriginalId = new HashMap<>(); + + void addExpandedId(IIdType theOriginalId, IIdType theExpandedId) { + assert isRemapCandidate(theOriginalId) : theOriginalId.getValue(); + myOriginalIdToExpandedId.add(theOriginalId); + myExpandedIdToOriginalId.put(theExpandedId, theOriginalId); + } + + public Optional getOriginalIdForExpandedId(IIdType theId) { + assert isRemapCandidate(theId) : theId.getValue(); + + // If we have this ID in the OriginalId map, it was explicitly + // searched for, so even if it's also an expanded ID we don't + // want to consider it as one + if (myOriginalIdToExpandedId.contains(theId)) { + return Optional.empty(); + } + + IIdType originalId = myExpandedIdToOriginalId.get(theId); + return Optional.ofNullable(originalId); + } + + public static boolean isRemapCandidate(IIdType theId) { + return theId != null + && !theId.isLocal() + && !theId.isUuid() + && theId.hasResourceType() + && theId.hasIdPart() + && theId.getValue().equals(theId.toUnqualifiedVersionless().getValue()); + } +} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSearchExpansionSvc.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSearchExpansionSvc.java index 804ed5310b6..ee44f855f2b 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSearchExpansionSvc.java +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSearchExpansionSvc.java @@ -19,17 +19,18 @@ */ package ca.uhn.fhir.mdm.svc; +import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.mdm.api.IMdmLinkExpandSvc; import ca.uhn.fhir.mdm.log.Logs; import ca.uhn.fhir.model.api.IQueryParameterType; -import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.rest.param.BaseParam; import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.param.TokenParam; +import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IIdType; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; @@ -37,17 +38,20 @@ import org.springframework.beans.factory.annotation.Autowired; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; -import java.util.function.Function; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; public class MdmSearchExpansionSvc { - // A simple interface to turn ids into some form of IQueryParameterTypes - private interface Creator { - T create(String id); - } - + private static final String EXPANSION_RESULTS = MdmSearchExpansionSvc.class.getName() + "_EXPANSION_RESULTS"; + private static final String RESOURCE_NAME = MdmSearchExpansionSvc.class.getName() + "_RESOURCE_NAME"; + private static final String QUERY_STRING = MdmSearchExpansionSvc.class.getName() + "_QUERY_STRING"; private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); + @Autowired + private FhirContext myFhirContext; + @Autowired private IRequestPartitionHelperSvc myRequestPartitionHelperSvc; @@ -64,66 +68,111 @@ public class MdmSearchExpansionSvc { * This is an internal MDM service and its API is subject to change. Use with caution! *

* - * @param theRequestDetails The incoming request details + * @param theRequestDetails The incoming request details * @param theSearchParameterMap The parameter map to modify - * @param theExpansionCandidateTester Each {@link ReferenceParam} in the map will be first tested - * by this function to determine whether it should be expanded. + * @param theParamTester Determines which parameters should be expanded + * @return Returns the results of the expansion, which are also stored in the {@link RequestDetails} userdata map, so this service will only be invoked a maximum of once per request. * @since 8.0.0 */ - public void expandSearch( - RequestDetails theRequestDetails, - SearchParameterMap theSearchParameterMap, - Function theExpansionCandidateTester) { - final RequestDetails requestDetailsToUse = - theRequestDetails == null ? new SystemRequestDetails() : theRequestDetails; + public MdmSearchExpansionResults expandSearchAndStoreInRequestDetails( + @Nullable RequestDetails theRequestDetails, + @Nonnull SearchParameterMap theSearchParameterMap, + IParamTester theParamTester) { + + if (theRequestDetails == null) { + return null; + } + + // Try to detect if the RequestDetails is being reused across multiple different queries, which + // can happen during CQL measure evaluation + String resourceName = theRequestDetails.getResourceName(); + String queryString = theSearchParameterMap.toNormalizedQueryString(myFhirContext); + if (!Objects.equals(resourceName, theRequestDetails.getUserData().get(RESOURCE_NAME)) + || !Objects.equals(queryString, theRequestDetails.getUserData().get(QUERY_STRING))) { + theRequestDetails.getUserData().remove(EXPANSION_RESULTS); + } + theRequestDetails.getUserData().put(RESOURCE_NAME, resourceName); + theRequestDetails.getUserData().put(QUERY_STRING, queryString); + + MdmSearchExpansionResults expansionResults = getCachedExpansionResults(theRequestDetails); + if (expansionResults != null) { + return expansionResults; + } + + expansionResults = new MdmSearchExpansionResults(); + final RequestPartitionId requestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequestForSearchType( - requestDetailsToUse, requestDetailsToUse.getResourceName(), theSearchParameterMap); + theRequestDetails, theRequestDetails.getResourceName(), theSearchParameterMap); + for (Map.Entry>> set : theSearchParameterMap.entrySet()) { String paramName = set.getKey(); List> andList = set.getValue(); for (List orList : andList) { // here we will know if it's an _id param or not // from theSearchParameterMap.keySet() - expandAnyReferenceParameters(requestPartitionId, paramName, orList, theExpansionCandidateTester); + expandAnyReferenceParameters( + requestPartitionId, + theRequestDetails.getResourceName(), + paramName, + orList, + theParamTester, + expansionResults); } } + + theRequestDetails.getUserData().put(EXPANSION_RESULTS, expansionResults); + + return expansionResults; } private void expandAnyReferenceParameters( RequestPartitionId theRequestPartitionId, + String theResourceName, String theParamName, List orList, - Function theExpansionCandidateTester) { + IParamTester theParamTester, + MdmSearchExpansionResults theResultsToPopulate) { + List toRemove = new ArrayList<>(); List toAdd = new ArrayList<>(); for (IQueryParameterType iQueryParameterType : orList) { if (iQueryParameterType instanceof ReferenceParam) { ReferenceParam refParam = (ReferenceParam) iQueryParameterType; - if (theExpansionCandidateTester.apply(refParam)) { + if (theParamTester.shouldExpand(theParamName, refParam)) { ourLog.debug("Found a reference parameter to expand: {}", refParam); // First, attempt to expand as a source resource. - Set expandedResourceIds = myMdmLinkExpandSvc.expandMdmBySourceResourceId( - theRequestPartitionId, new IdDt(refParam.getValue())); + IIdType sourceId = newId(refParam.getValue()); + Set expandedResourceIds = + myMdmLinkExpandSvc.expandMdmBySourceResourceId(theRequestPartitionId, sourceId); // If we failed, attempt to expand as a golden resource if (expandedResourceIds.isEmpty()) { - expandedResourceIds = myMdmLinkExpandSvc.expandMdmByGoldenResourceId( - theRequestPartitionId, new IdDt(refParam.getValue())); + expandedResourceIds = + myMdmLinkExpandSvc.expandMdmByGoldenResourceId(theRequestPartitionId, sourceId); } // Rebuild the search param list. if (!expandedResourceIds.isEmpty()) { ourLog.debug("Parameter has been expanded to: {}", String.join(", ", expandedResourceIds)); toRemove.add(refParam); - expandedResourceIds.stream() - .map(resourceId -> addResourceTypeIfNecessary(refParam.getResourceType(), resourceId)) - .map(ReferenceParam::new) - .forEach(toAdd::add); + for (String resourceId : expandedResourceIds) { + IIdType nextReference = + newId(addResourceTypeIfNecessary(refParam.getResourceType(), resourceId)); + toAdd.add(new ReferenceParam(nextReference)); + theResultsToPopulate.addExpandedId(sourceId, nextReference); + } } } - } else if (theParamName.equalsIgnoreCase("_id")) { - expandIdParameter(theRequestPartitionId, iQueryParameterType, toAdd, toRemove); + } else if (theParamName.equalsIgnoreCase(IAnyResource.SP_RES_ID)) { + expandIdParameter( + theRequestPartitionId, + iQueryParameterType, + toAdd, + toRemove, + theParamTester, + theResourceName, + theResultsToPopulate); } } @@ -131,6 +180,10 @@ public class MdmSearchExpansionSvc { orList.addAll(toAdd); } + private IIdType newId(String value) { + return myFhirContext.getVersion().newIdType(value); + } + private String addResourceTypeIfNecessary(String theResourceType, String theResourceId) { if (theResourceId.contains("/")) { return theResourceId; @@ -142,17 +195,15 @@ public class MdmSearchExpansionSvc { /** * Expands out the provided _id parameter into all the various * ids of linked resources. - * - * @param theRequestPartitionId - * @param theIdParameter - * @param theAddList - * @param theRemoveList */ private void expandIdParameter( RequestPartitionId theRequestPartitionId, IQueryParameterType theIdParameter, List theAddList, - List theRemoveList) { + List theRemoveList, + IParamTester theParamTester, + String theResourceName, + MdmSearchExpansionResults theResultsToPopulate) { // id parameters can either be StringParam (for $everything operation) // or TokenParam (for searches) // either case, we want to expand it out and grab all related resources @@ -161,8 +212,10 @@ public class MdmSearchExpansionSvc { boolean mdmExpand = false; if (theIdParameter instanceof TokenParam) { TokenParam param = (TokenParam) theIdParameter; - mdmExpand = param.isMdmExpand(); - id = new IdDt(param.getValue()); + mdmExpand = theParamTester.shouldExpand(IAnyResource.SP_RES_ID, param); + String value = param.getValue(); + value = addResourceTypeIfNecessary(theResourceName, value); + id = newId(value); creator = TokenParam::new; } else { creator = null; @@ -180,20 +233,43 @@ public class MdmSearchExpansionSvc { Set expandedResourceIds = myMdmLinkExpandSvc.expandMdmBySourceResourceId(theRequestPartitionId, id); if (expandedResourceIds.isEmpty()) { - expandedResourceIds = myMdmLinkExpandSvc.expandMdmByGoldenResourceId(theRequestPartitionId, (IdDt) id); + expandedResourceIds = myMdmLinkExpandSvc.expandMdmByGoldenResourceId(theRequestPartitionId, id); } // Rebuild if (!expandedResourceIds.isEmpty()) { - ourLog.debug("_id parameter has been expanded to: {}", String.join(", ", expandedResourceIds)); + ourLog.debug("_id parameter has been expanded to: {}", expandedResourceIds); // remove the original theRemoveList.add(theIdParameter); // add in all the linked values expandedResourceIds.stream().map(creator::create).forEach(theAddList::add); + + for (String expandedId : expandedResourceIds) { + theResultsToPopulate.addExpandedId( + id, newId(addResourceTypeIfNecessary(theResourceName, expandedId))); + } } } // else - no expansion required } + + // A simple interface to turn ids into some form of IQueryParameterTypes + private interface Creator { + T create(String id); + } + + @FunctionalInterface + public interface IParamTester { + + boolean shouldExpand(String theParamName, BaseParam theParam); + } + + @Nullable + public static MdmSearchExpansionResults getCachedExpansionResults(@Nonnull RequestDetails theRequestDetails) { + MdmSearchExpansionResults expansionResults = + (MdmSearchExpansionResults) theRequestDetails.getUserData().get(EXPANSION_RESULTS); + return expansionResults; + } }