Test cleanup

This commit is contained in:
jamesagnew 2024-11-17 16:49:28 -05:00
parent e43df9f996
commit fe1fc3a928
9 changed files with 468 additions and 251 deletions

View File

@ -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>

View File

@ -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

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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());
}
}

View File

@ -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;
}
}