Test cleanup
This commit is contained in:
parent
e43df9f996
commit
fe1fc3a928
|
@ -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 <code>IFhirVersionServer<code> for this version.
|
||||
* Note that this method may only be called if the <code>hapi-fhir-server</code>
|
||||
|
|
|
@ -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<String> 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<String> 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<String> 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<String, IBaseResource> 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<String> ids = toUnqualifiedVersionlessIdValues(outcome);
|
||||
assertThat(ids).asList().containsExactlyInAnyOrder(
|
||||
myGoldenResourcePatientAId.getValue(),
|
||||
mySourcePatientA2Id.getValue(),
|
||||
myObservationReferencingGoldenPatientAId.getValue(),
|
||||
myObservationReferencingSourcePatientA0Id.getValue(),
|
||||
myObservationReferencingSourcePatientA1Id.getValue(),
|
||||
myObservationReferencingSourcePatientA2Id.getValue()
|
||||
);
|
||||
Map<String, IBaseResource> 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<String> 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<String> 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<String, IBaseResource> 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
|
||||
|
|
|
@ -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<String> expandMdmByGoldenResourcePid(
|
||||
RequestPartitionId theRequestPartitionId, IResourcePersistentId<?> theGoldenResourcePid);
|
||||
|
||||
Set<String> expandMdmByGoldenResourceId(RequestPartitionId theRequestPartitionId, IdDt theId);
|
||||
Set<String> expandMdmByGoldenResourceId(RequestPartitionId theRequestPartitionId, IIdType theId);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
* <b>This class is experimental and subject to change. Use with caution.</b>
|
||||
|
@ -75,9 +73,13 @@ import java.util.Set;
|
|||
* @since 8.0.0
|
||||
*/
|
||||
public class MdmReadVirtualizationInterceptor<P extends IResourcePersistentId<?>> {
|
||||
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<P extends IResourcePersistentId<?>
|
|||
@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<String, Integer> candidateResourceIds = extractRemapCandidateResources(theDetails);
|
||||
ListMultimap<String, ResourceReferenceInfo> candidateReferences = extractRemapCandidateReferences(theDetails);
|
||||
|
||||
CandidateMdmLinkedResources<P> candidates =
|
||||
findCandidateMdmLinkedResources(candidateResourceIds, candidateReferences);
|
||||
|
||||
// Loop through each link and figure out whether we need to remap anything
|
||||
for (MdmPidTuple<P> tuple : candidates.getTuples()) {
|
||||
Optional<String> sourceIdOpt = candidates.getFhirIdForPersistentId(tuple.getSourcePid());
|
||||
if (sourceIdOpt.isPresent()) {
|
||||
String sourceId = sourceIdOpt.get();
|
||||
|
||||
// Remap references from source to golden
|
||||
List<ResourceReferenceInfo> referencesToRemap = candidateReferences.get(sourceId);
|
||||
if (!referencesToRemap.isEmpty()) {
|
||||
P associatedGoldenResourcePid = tuple.getGoldenPid();
|
||||
Optional<String> associatedGoldenResourceId =
|
||||
candidates.getFhirIdForPersistentId(associatedGoldenResourcePid);
|
||||
if (associatedGoldenResourceId.isPresent()) {
|
||||
for (ResourceReferenceInfo referenceInfoToRemap : referencesToRemap) {
|
||||
IBaseReference referenceToRemap = referenceInfoToRemap.getResourceReference();
|
||||
referenceToRemap.setReference(associatedGoldenResourceId.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out source resources
|
||||
Optional<String> 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<P> findCandidateMdmLinkedResources(
|
||||
ListMultimap<String, Integer> candidateResourceIds,
|
||||
ListMultimap<String, ResourceReferenceInfo> 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<IIdType> allIds = new HashSet<>();
|
||||
candidateResourceIds.keySet().forEach(t -> allIds.add(newIdType(t)));
|
||||
candidateReferences.keySet().forEach(t -> allIds.add(newIdType(t)));
|
||||
List<P> sourcePids =
|
||||
myIdHelperService.getPidsOrThrowException(RequestPartitionId.allPartitions(), List.copyOf(allIds));
|
||||
Collection<MdmPidTuple<P>> tuples = myMdmLinkDao.resolveGoldenResources(sourcePids);
|
||||
|
||||
// Resolve the link PIDs into FHIR IDs
|
||||
Set<P> allPersistentIds = new HashSet<>();
|
||||
tuples.forEach(t -> allPersistentIds.add(t.getGoldenPid()));
|
||||
tuples.forEach(t -> allPersistentIds.add(t.getSourcePid()));
|
||||
PersistentIdToForcedIdMap<P> 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<String, Integer> extractRemapCandidateResources(IPreResourceShowDetails theDetails) {
|
||||
ListMultimap<String, Integer> 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<IIdType> 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<IIdType> 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<String, ResourceReferenceInfo> extractRemapCandidateReferences(
|
||||
IPreResourceShowDetails theDetails) {
|
||||
ListMultimap<String, ResourceReferenceInfo> 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<ResourceReferenceInfo> 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<ResourceReferenceInfo> referenceInfos = terser.getAllResourceReferences(resource);
|
||||
for (ResourceReferenceInfo referenceInfo : referenceInfos) {
|
||||
IIdType referenceId = referenceInfo
|
||||
.getResourceReference()
|
||||
.getReferenceElement()
|
||||
.toUnqualifiedVersionless();
|
||||
if (referenceId.hasResourceType()
|
||||
&& referenceId.hasIdPart()
|
||||
&& !referenceId.isLocal()
|
||||
&& !referenceId.isUuid()) {
|
||||
Optional<IIdType> 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<P extends IResourcePersistentId<?>> {
|
||||
private final Collection<MdmPidTuple<P>> myTuples;
|
||||
private final PersistentIdToForcedIdMap<P> myPersistentIdToFhirIdMap;
|
||||
|
||||
public CandidateMdmLinkedResources(
|
||||
Collection<MdmPidTuple<P>> thePidTuples, PersistentIdToForcedIdMap<P> thePersistentIdToForcedIdMap) {
|
||||
this.myTuples = thePidTuples;
|
||||
this.myPersistentIdToFhirIdMap = thePersistentIdToForcedIdMap;
|
||||
}
|
||||
|
||||
public Collection<MdmPidTuple<P>> getTuples() {
|
||||
return myTuples;
|
||||
}
|
||||
|
||||
public Optional<String> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String> expandMdmByGoldenResourceId(RequestPartitionId theRequestPartitionId, IdDt theId) {
|
||||
public Set<String> expandMdmByGoldenResourceId(RequestPartitionId theRequestPartitionId, IIdType theId) {
|
||||
ourLog.debug("About to expand golden resource with golden resource id {}", theId);
|
||||
IResourcePersistentId<?> pidOrThrowException =
|
||||
myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theId);
|
||||
|
|
|
@ -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<IIdType> myOriginalIdToExpandedId = new HashSet<>();
|
||||
private Map<IIdType, IIdType> myExpandedIdToOriginalId = new HashMap<>();
|
||||
|
||||
void addExpandedId(IIdType theOriginalId, IIdType theExpandedId) {
|
||||
assert isRemapCandidate(theOriginalId) : theOriginalId.getValue();
|
||||
myOriginalIdToExpandedId.add(theOriginalId);
|
||||
myExpandedIdToOriginalId.put(theExpandedId, theOriginalId);
|
||||
}
|
||||
|
||||
public Optional<IIdType> 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());
|
||||
}
|
||||
}
|
|
@ -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 extends IQueryParameterType> {
|
||||
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!
|
||||
* </p>
|
||||
*
|
||||
* @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<ReferenceParam, Boolean> 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<String, List<List<IQueryParameterType>>> set : theSearchParameterMap.entrySet()) {
|
||||
String paramName = set.getKey();
|
||||
List<List<IQueryParameterType>> andList = set.getValue();
|
||||
for (List<IQueryParameterType> 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<IQueryParameterType> orList,
|
||||
Function<ReferenceParam, Boolean> theExpansionCandidateTester) {
|
||||
IParamTester theParamTester,
|
||||
MdmSearchExpansionResults theResultsToPopulate) {
|
||||
|
||||
List<IQueryParameterType> toRemove = new ArrayList<>();
|
||||
List<IQueryParameterType> 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<String> expandedResourceIds = myMdmLinkExpandSvc.expandMdmBySourceResourceId(
|
||||
theRequestPartitionId, new IdDt(refParam.getValue()));
|
||||
IIdType sourceId = newId(refParam.getValue());
|
||||
Set<String> 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<IQueryParameterType> theAddList,
|
||||
List<IQueryParameterType> theRemoveList) {
|
||||
List<IQueryParameterType> 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<String> 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 extends IQueryParameterType> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue