Reverse changes from 5493. Ensure MDM expansion is limited by queried partition. Support $everything with MDM expansion and POST (#5585)

* Reverse changes from 5493.  Add a stub changelist.

* Add various changes including stub changelist, beginnings of integration test, pass request details to interceptor, obtain partition ID, and stub code for each partition-aware query.

* Spotless.  Implement methods and SQLs for both golden and source IDs.  Not yet tested.  Add default interface methods.

* Fix HQLs that were referring to incorrect columns.  Rename parameter.  Formatting.

* Fix logic to use single queries by source and golden resource.  Filter by partition IDs in Java instead of in SQL.  Write a new unit test for MdmLinkExpandSvc.

* Spotless.

* Ensure $everything with mdm expansion works on POST as well as GET.  Some cleanup.

* More cleanup.

* Null safety for mdmExpand.

* Spotless.

* Small changes.

* Enhance changelog.

* Delete commented out code.

* First round of code review changes.

* More code review changes.
This commit is contained in:
Luke deGruchy 2024-01-12 13:46:15 -05:00 committed by GitHub
parent d6128dece0
commit 763894c28f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 443 additions and 141 deletions

View File

@ -105,6 +105,10 @@ public class RequestPartitionId implements IModelJson {
return myAllPartitions;
}
public boolean isPartitionCovered(Integer thePartitionId) {
return isAllPartitions() || getPartitionIds().contains(thePartitionId);
}
@Nullable
public LocalDate getPartitionDate() {
return myPartitionDate;

View File

@ -219,6 +219,7 @@ public class Constants {
public static final String PARAM_TAGS = "_tags";
public static final String PARAM_TEXT = "_text";
public static final String PARAM_VALIDATE = "_validate";
public static final String PARAM_MDM = "_mdm";
public static final String PARAMQUALIFIER_MISSING = ":missing";
public static final String PARAMQUALIFIER_MISSING_FALSE = "false";

View File

@ -0,0 +1,7 @@
---
type: fix
title: "$everything queries with MDM expansion were bypassing the partition boundary by return resources outside the partition.
Also, POST (as opposed to GET) $everything queries with MDM expansion.
The first issue was fixed by reversing the previous changes in [5493](https://github.com/hapifhir/hapi-fhir/issues/5493)
and by filtering source and golden resources by partition ID.
The second issue was fixed by correctly capturing the MDM expansion flag in POST $everything queries."

View File

@ -46,7 +46,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.Collections;
public class JpaResourceDaoPatient<T extends IBaseResource> extends BaseHapiFhirResourceDao<T>
@ -67,6 +66,7 @@ public class JpaResourceDaoPatient<T extends IBaseResource> extends BaseHapiFhir
StringAndListParam theNarrative,
StringAndListParam theFilter,
StringAndListParam theTypes,
boolean theMdmExpand,
RequestDetails theRequest) {
SearchParameterMap paramMap = new SearchParameterMap();
if (theCount != null) {
@ -95,11 +95,8 @@ public class JpaResourceDaoPatient<T extends IBaseResource> extends BaseHapiFhir
paramMap.setSort(theSort);
paramMap.setLastUpdated(theLastUpdated);
if (theIds != null) {
if (theRequest.getParameters().containsKey("_mdm")) {
String[] paramVal = theRequest.getParameters().get("_mdm");
if (Arrays.asList(paramVal).contains("true")) {
theIds.getValuesAsQueryTokens().forEach(param -> param.setMdmExpand(true));
}
if (theMdmExpand) {
theIds.getValuesAsQueryTokens().forEach(param -> param.setMdmExpand(true));
}
paramMap.add("_id", theIds);
}
@ -161,6 +158,7 @@ public class JpaResourceDaoPatient<T extends IBaseResource> extends BaseHapiFhir
theQueryParams.getNarrative(),
theQueryParams.getFilter(),
theQueryParams.getTypes(),
theQueryParams.getMdmExpand(),
theRequestDetails);
}
@ -181,6 +179,7 @@ public class JpaResourceDaoPatient<T extends IBaseResource> extends BaseHapiFhir
theQueryParams.getNarrative(),
theQueryParams.getFilter(),
theQueryParams.getTypes(),
theQueryParams.getMdmExpand(),
theRequestDetails);
}
}

View File

@ -57,16 +57,22 @@ public interface IMdmLinkJpaRepository
nativeQuery = true)
void deleteLinksHistoryWithAnyReferenceToPids(@Param("goldenPids") List<Long> theResourcePids);
@Query("SELECT ml2.myGoldenResourcePid as goldenPid, ml2.mySourcePid as sourcePid FROM MdmLink ml2 "
+ "WHERE ml2.myMatchResult=:matchResult "
+ "AND ml2.myGoldenResourcePid IN ("
+ "SELECT ml.myGoldenResourcePid FROM MdmLink ml "
+ "INNER JOIN ResourceLink hrl "
+ "ON hrl.myTargetResourcePid=ml.mySourcePid "
+ "AND hrl.mySourceResourcePid=:groupPid "
+ "AND hrl.mySourcePath='Group.member.entity' "
+ "AND hrl.myTargetResourceType='Patient'"
+ ")")
// TODO: LD: the calling code in JpaBulkExportProcessor doesn't yet leverage the partition IDs, but maybe it
// should?
@Query(
"SELECT lookup_links.myGoldenResourcePid as goldenPid, gld_rt.myPartitionIdValue as goldenPartitionId, lookup_links.mySourcePid as sourcePid, lookup_links.myPartitionIdValue as sourcePartitionId "
+ "FROM MdmLink lookup_links "
+ "INNER JOIN ResourceTable gld_rt "
+ "on lookup_links.myGoldenResourcePid=gld_rt.myId "
+ "WHERE lookup_links.myMatchResult=:matchResult "
+ "AND lookup_links.myGoldenResourcePid IN ("
+ "SELECT inner_mdm_link.myGoldenResourcePid FROM MdmLink inner_mdm_link "
+ "INNER JOIN ResourceLink inner_res_link "
+ "ON inner_res_link.myTargetResourcePid=inner_mdm_link.mySourcePid "
+ "AND inner_res_link.mySourceResourcePid=:groupPid "
+ "AND inner_res_link.mySourcePath='Group.member.entity' "
+ "AND inner_res_link.myTargetResourceType='Patient'"
+ ")")
List<MdmPidTuple> expandPidsFromGroupPidGivenMatchResult(
@Param("groupPid") Long theGroupPid, @Param("matchResult") MdmMatchResultEnum theMdmMatchResultEnum);
@ -77,15 +83,23 @@ public interface IMdmLinkJpaRepository
interface MdmPidTuple {
Long getGoldenPid();
Integer getGoldenPartitionId();
Long getSourcePid();
Integer getSourcePartitionId();
}
@Query("SELECT ml.myGoldenResourcePid as goldenPid, ml.mySourcePid as sourcePid " + "FROM MdmLink ml "
+ "INNER JOIN MdmLink ml2 "
+ "on ml.myGoldenResourcePid=ml2.myGoldenResourcePid "
+ "WHERE ml2.mySourcePid=:sourcePid "
+ "AND ml2.myMatchResult=:matchResult "
+ "AND ml.myMatchResult=:matchResult")
@Query(
"SELECT lookup_link.myGoldenResourcePid as goldenPid, gld_rt.myPartitionIdValue as goldenPartitionId, lookup_link.mySourcePid as sourcePid, lookup_link.myPartitionIdValue as sourcePartitionId "
+ "FROM MdmLink lookup_link "
+ "INNER JOIN MdmLink gld_link "
+ "on lookup_link.myGoldenResourcePid=gld_link.myGoldenResourcePid "
+ "INNER JOIN ResourceTable gld_rt "
+ "on gld_link.myGoldenResourcePid=gld_rt.myId "
+ "WHERE gld_link.mySourcePid=:sourcePid "
+ "AND gld_link.myMatchResult=:matchResult "
+ "AND lookup_link.myMatchResult=:matchResult")
List<MdmPidTuple> expandPidsBySourcePidAndMatchResult(
@Param("sourcePid") Long theSourcePid, @Param("matchResult") MdmMatchResultEnum theMdmMatchResultEnum);
@ -99,7 +113,12 @@ public interface IMdmLinkJpaRepository
@Param("matchResult") MdmMatchResultEnum theMdmMatchResultEnumToExclude);
@Query(
"SELECT ml.myGoldenResourcePid as goldenPid, ml.mySourcePid as sourcePid FROM MdmLink ml WHERE ml.myGoldenResourcePid = :goldenPid and ml.myMatchResult = :matchResult")
"SELECT lookup_link.myGoldenResourcePid as goldenPid, gld_rt.myPartitionIdValue as goldenPartitionId, lookup_link.mySourcePid as sourcePid, lookup_link.myPartitionIdValue as sourcePartitionId "
+ "FROM MdmLink lookup_link "
+ "INNER JOIN ResourceTable gld_rt "
+ "on lookup_link.myGoldenResourcePid=gld_rt.myId "
+ "WHERE lookup_link.myGoldenResourcePid = :goldenPid "
+ "AND lookup_link.myMatchResult = :matchResult")
List<MdmPidTuple> expandPidsByGoldenResourcePidAndMatchResult(
@Param("goldenPid") Long theSourcePid, @Param("matchResult") MdmMatchResultEnum theMdmMatchResultEnum);

View File

@ -59,6 +59,8 @@ 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.IdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionSynchronizationManager;
@ -98,6 +100,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
*/
@Service
public class IdHelperService implements IIdHelperService<JpaPid> {
private static final Logger ourLog = LoggerFactory.getLogger(IdHelperService.class);
public static final Predicate[] EMPTY_PREDICATE_ARRAY = new Predicate[0];
public static final String RESOURCE_PID = "RESOURCE_PID";
@ -210,9 +213,6 @@ public class IdHelperService implements IIdHelperService<JpaPid> {
Validate.isTrue(!theIds.isEmpty(), "theIds must not be empty");
Map<String, JpaPid> retVals = new HashMap<>();
RequestPartitionId partitionId = myPartitionSettings.isAllowUnqualifiedCrossPartitionReference()
? RequestPartitionId.allPartitions()
: theRequestPartitionId;
for (String id : theIds) {
JpaPid retVal;
if (!idRequiresForcedId(id)) {
@ -223,17 +223,18 @@ public class IdHelperService implements IIdHelperService<JpaPid> {
// is a forced id
// we must resolve!
if (myStorageSettings.isDeleteEnabled()) {
retVal = resolveResourceIdentity(partitionId, theResourceType, id, theExcludeDeleted)
retVal = resolveResourceIdentity(theRequestPartitionId, theResourceType, id, theExcludeDeleted)
.getPersistentId();
retVals.put(id, retVal);
} else {
// fetch from cache... adding to cache if not available
String key = toForcedIdToPidKey(partitionId, theResourceType, id);
String key = toForcedIdToPidKey(theRequestPartitionId, theResourceType, id);
retVal = myMemoryCacheService.getThenPutAfterCommit(
MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key, t -> {
List<IIdType> ids = Collections.singletonList(new IdType(theResourceType, id));
// fetches from cache using a function that checks cache first...
List<JpaPid> resolvedIds = resolveResourcePersistentIdsWithCache(partitionId, ids);
List<JpaPid> resolvedIds =
resolveResourcePersistentIdsWithCache(theRequestPartitionId, ids);
if (resolvedIds.isEmpty()) {
throw new ResourceNotFoundException(Msg.code(1100) + ids.get(0));
}

View File

@ -120,8 +120,11 @@ public class MdmLinkDaoJpaImpl implements IMdmLinkDao<JpaPid, MdmLink> {
}
private MdmPidTuple<JpaPid> daoTupleToMdmTuple(IMdmLinkJpaRepository.MdmPidTuple theMdmPidTuple) {
return MdmPidTuple.fromGoldenAndSource(
JpaPid.fromId(theMdmPidTuple.getGoldenPid()), JpaPid.fromId(theMdmPidTuple.getSourcePid()));
return MdmPidTuple.fromGoldenAndSourceAndPartitionIds(
JpaPid.fromId(theMdmPidTuple.getGoldenPid()),
theMdmPidTuple.getGoldenPartitionId(),
JpaPid.fromId(theMdmPidTuple.getSourcePid()),
theMdmPidTuple.getSourcePartitionId());
}
@Override

View File

@ -112,6 +112,11 @@ public abstract class BaseJpaResourceProviderPatient<T extends IBaseResource> ex
max = OperationParam.MAX_UNLIMITED,
typeName = "string")
List<IPrimitiveType<String>> theTypes,
@Description(
shortDefinition =
"Filter the resources to return only resources matching the given _type filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)")
@OperationParam(name = Constants.PARAM_MDM, min = 0, max = 1, typeName = "boolean")
IPrimitiveType<Boolean> theMdmExpand,
@Sort SortSpec theSortSpec,
RequestDetails theRequestDetails) {
@ -126,6 +131,7 @@ public abstract class BaseJpaResourceProviderPatient<T extends IBaseResource> ex
everythingParams.setNarrative(toStringAndList(theNarrative));
everythingParams.setFilter(toStringAndList(theFilter));
everythingParams.setTypes(toStringAndList(theTypes));
everythingParams.setMdmExpand(resolveNullValue(theMdmExpand));
return ((IFhirResourceDaoPatient<?>) getDao())
.patientInstanceEverything(theServletRequest, theRequestDetails, everythingParams, theId);
@ -202,6 +208,11 @@ public abstract class BaseJpaResourceProviderPatient<T extends IBaseResource> ex
max = OperationParam.MAX_UNLIMITED,
typeName = "id")
List<IIdType> theId,
@Description(
shortDefinition =
"Filter the resources to return only resources matching the given _type filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)")
@OperationParam(name = Constants.PARAM_MDM, min = 0, max = 1, typeName = "boolean")
IPrimitiveType<Boolean> theMdmExpand,
@Sort SortSpec theSortSpec,
RequestDetails theRequestDetails) {
@ -216,6 +227,7 @@ public abstract class BaseJpaResourceProviderPatient<T extends IBaseResource> ex
everythingParams.setNarrative(toStringAndList(theNarrative));
everythingParams.setFilter(toStringAndList(theFilter));
everythingParams.setTypes(toStringAndList(theTypes));
everythingParams.setMdmExpand(resolveNullValue(theMdmExpand));
return ((IFhirResourceDaoPatient<?>) getDao())
.patientTypeEverything(
@ -261,4 +273,8 @@ public abstract class BaseJpaResourceProviderPatient<T extends IBaseResource> ex
}
return retVal;
}
private boolean resolveNullValue(IPrimitiveType<Boolean> theMdmExpand) {
return theMdmExpand == null ? Boolean.FALSE : theMdmExpand.getValue();
}
}

View File

@ -176,7 +176,7 @@ public class JpaBulkExportProcessorTest {
}
private MdmPidTuple<JpaPid> createTuple(long theGroupId, long theGoldenId) {
return MdmPidTuple.fromGoldenAndSource(JpaPid.fromId(theGoldenId), JpaPid.fromId(theGroupId));
return MdmPidTuple.fromGoldenAndSourceAndPartitionIds(JpaPid.fromId(theGoldenId), null, JpaPid.fromId(theGroupId), null);
}
@ParameterizedTest

View File

@ -8,11 +8,11 @@ import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.util.MemoryCacheService;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Tuple;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.*;
import org.hl7.fhir.r4.hapi.ctx.FhirR4;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Root;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -22,9 +22,6 @@ import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.Instant;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
@ -60,15 +57,14 @@ public class IdHelperServiceTest {
void setUp() {
subject.setDontCheckActiveTransactionForUnitTest(true);
when(myStorageSettings.isDeleteEnabled()).thenReturn(true);
when(myStorageSettings.getResourceClientIdStrategy()).thenReturn(JpaStorageSettings.ClientIdStrategyEnum.ANY);
when(myPartitionSettings.isAllowUnqualifiedCrossPartitionReference()).thenReturn(true);
when(myStorageSettings.isDeleteEnabled()).thenReturn(true);
when(myStorageSettings.getResourceClientIdStrategy()).thenReturn(JpaStorageSettings.ClientIdStrategyEnum.ANY);
}
@Test
public void testResolveResourcePersistentIds() {
//prepare params
RequestPartitionId requestPartitionId = RequestPartitionId.fromPartitionName("Partition-A");
RequestPartitionId requestPartitionId = RequestPartitionId.fromPartitionIdAndName(1, "Partition-A");
String resourceType = "Patient";
Long id = 123L;
List<String> ids = List.of(String.valueOf(id));
@ -77,25 +73,18 @@ public class IdHelperServiceTest {
//prepare results
Patient expectedPatient = new Patient();
expectedPatient.setId(ids.get(0));
Object[] obj = new Object[] {resourceType, Long.parseLong(ids.get(0)), ids.get(0), Date.from(Instant.now())};
// configure mock behaviour
when(myStorageSettings.isDeleteEnabled()).thenReturn(true);
when(myResourceTableDao
.findAndResolveByForcedIdWithNoType(eq(resourceType), eq(ids), eq(theExcludeDeleted)))
.thenReturn(Collections.singletonList(obj));
when(myStorageSettings.isDeleteEnabled()).thenReturn(true);
Map<String, JpaPid> actualIds = subject.resolveResourcePersistentIds(requestPartitionId, resourceType, ids, theExcludeDeleted);
//verify results
assertFalse(actualIds.isEmpty());
assertEquals(id, actualIds.get(ids.get(0)).getId());
final ResourceNotFoundException resourceNotFoundException = assertThrows(ResourceNotFoundException.class, () -> subject.resolveResourcePersistentIds(requestPartitionId, resourceType, ids, theExcludeDeleted));
assertEquals("HAPI-2001: Resource Patient/123 is not known", resourceNotFoundException.getMessage());
}
@Test
public void testResolveResourcePersistentIdsDeleteFalse() {
//prepare Params
RequestPartitionId requestPartitionId = RequestPartitionId.fromPartitionName("Partition-A");
RequestPartitionId requestPartitionId = RequestPartitionId.fromPartitionIdAndName(1, "Partition-A");
Long id = 123L;
String resourceType = "Patient";
List<String> ids = List.of(String.valueOf(id));
@ -107,54 +96,21 @@ public class IdHelperServiceTest {
expectedPatient.setId(ids.get(0));
// configure mock behaviour
configureCacheBehaviour(forcedId);
configureEntityManagerBehaviour(id, resourceType, ids.get(0));
when(myStorageSettings.isDeleteEnabled()).thenReturn(false);
when(myFhirCtx.getVersion()).thenReturn(new FhirR4());
Map<String, JpaPid> actualIds = subject.resolveResourcePersistentIds(requestPartitionId, resourceType, ids, theExcludeDeleted);
Map<String, JpaPid> actualIds = subject.resolveResourcePersistentIds(requestPartitionId, resourceType, ids, theExcludeDeleted);
//verifyResult
assertFalse(actualIds.isEmpty());
assertEquals(id, actualIds.get(ids.get(0)).getId());
//verifyResult
assertFalse(actualIds.isEmpty());
assertNull(actualIds.get(ids.get(0)));
}
private void configureCacheBehaviour(String resourceUrl) {
when(myMemoryCacheService.getThenPutAfterCommit(eq(MemoryCacheService.CacheEnum.FORCED_ID_TO_PID), eq(resourceUrl), any())).thenCallRealMethod();
doNothing().when(myMemoryCacheService).putAfterCommit(eq(MemoryCacheService.CacheEnum.FORCED_ID_TO_PID), eq(resourceUrl), ArgumentMatchers.<JpaPid>any());
when(myMemoryCacheService.getIfPresent(eq(MemoryCacheService.CacheEnum.FORCED_ID_TO_PID), eq(resourceUrl))).thenReturn(null);
}
private void configureEntityManagerBehaviour(Long idNumber, String resourceType, String id) {
List<Tuple> mockedTupleList = getMockedTupleList(idNumber, resourceType, id);
CriteriaBuilder builder = getMockedCriteriaBuilder();
Root<ResourceTable> from = getMockedFrom();
@SuppressWarnings("unchecked")
TypedQuery<Tuple> query = (TypedQuery<Tuple>) mock(TypedQuery.class);
@SuppressWarnings("unchecked")
CriteriaQuery<Tuple> cq = mock(CriteriaQuery.class);
when(builder.createTupleQuery()).thenReturn(cq);
when(cq.from(ArgumentMatchers.<Class<ResourceTable>>any())).thenReturn(from);
when(query.getResultList()).thenReturn(mockedTupleList);
when(myEntityManager.getCriteriaBuilder()).thenReturn(builder);
when(myEntityManager.createQuery(ArgumentMatchers.<CriteriaQuery<Tuple>>any())).thenReturn(query);
}
private CriteriaBuilder getMockedCriteriaBuilder() {
Predicate pred = mock(Predicate.class);
CriteriaBuilder builder = mock(CriteriaBuilder.class);
lenient().when(builder.equal(any(), any())).thenReturn(pred);
return builder;
}
private Root<ResourceTable> getMockedFrom() {
@SuppressWarnings("unchecked")
Path<Object> path = mock(Path.class);
@SuppressWarnings("unchecked")
Root<ResourceTable> from = mock(Root.class);
lenient().when(from.get(ArgumentMatchers.<String>any())).thenReturn(path);
when(from.get(ArgumentMatchers.<String>any())).thenReturn(path);
return from;
}

View File

@ -229,7 +229,6 @@ public class MdmSearchExpandingInterceptorIT extends BaseMdmR4Test {
String id = expectedIds.get(0);
HashMap<String, String[]> queryParameters = new HashMap<>();
queryParameters.put("_mdm", new String[]{"true"});
HttpServletRequest req = Mockito.mock(HttpServletRequest.class);
RequestDetails theDetails = Mockito.mock(RequestDetails.class);
@ -240,9 +239,11 @@ public class MdmSearchExpandingInterceptorIT extends BaseMdmR4Test {
// test
myStorageSettings.setAllowMdmExpansion(true);
IFhirResourceDaoPatient<Patient> dao = (IFhirResourceDaoPatient<Patient>) myPatientDao;
final PatientEverythingParameters queryParams = new PatientEverythingParameters();
queryParams.setMdmExpand(true);
IBundleProvider outcome = dao.patientInstanceEverything(
req,
theDetails, new PatientEverythingParameters(), new IdDt(id)
theDetails, queryParams, new IdDt(id)
);
// verify return results

View File

@ -19,6 +19,7 @@
*/
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;
@ -27,15 +28,18 @@ import org.hl7.fhir.instance.model.api.IIdType;
import java.util.Set;
public interface IMdmLinkExpandSvc {
Set<String> expandMdmBySourceResource(IBaseResource theResource);
Set<String> expandMdmBySourceResource(RequestPartitionId theRequestPartitionId, IBaseResource theResource);
Set<String> expandMdmBySourceResourceId(IIdType theId);
Set<String> expandMdmBySourceResourceId(RequestPartitionId theRequestPartitionId, IIdType theId);
Set<String> expandMdmBySourceResourcePid(IResourcePersistentId theSourceResourcePid);
Set<String> expandMdmBySourceResourcePid(
RequestPartitionId theRequestPartitionId, IResourcePersistentId<?> theSourceResourcePid);
Set<String> expandMdmByGoldenResourceId(IResourcePersistentId theGoldenResourcePid);
Set<String> expandMdmByGoldenResourceId(
RequestPartitionId theRequestPartitionId, IResourcePersistentId<?> theGoldenResourcePid);
Set<String> expandMdmByGoldenResourcePid(IResourcePersistentId theGoldenResourcePid);
Set<String> expandMdmByGoldenResourcePid(
RequestPartitionId theRequestPartitionId, IResourcePersistentId<?> theGoldenResourcePid);
Set<String> expandMdmByGoldenResourceId(IdDt theId);
Set<String> expandMdmByGoldenResourceId(RequestPartitionId theRequestPartitionId, IdDt theId);
}

View File

@ -22,12 +22,16 @@ package ca.uhn.fhir.mdm.interceptor;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
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.ReferenceParam;
import ca.uhn.fhir.rest.param.TokenParam;
import org.hl7.fhir.instance.model.api.IIdType;
@ -52,6 +56,9 @@ public class MdmSearchExpandingInterceptor {
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
@Autowired
private IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
@Autowired
private IMdmLinkExpandSvc myMdmLinkExpandSvc;
@ -59,15 +66,20 @@ public class MdmSearchExpandingInterceptor {
private JpaStorageSettings myStorageSettings;
@Hook(Pointcut.STORAGE_PRESEARCH_REGISTERED)
public void hook(SearchParameterMap theSearchParameterMap) {
public void hook(RequestDetails theRequestDetails, SearchParameterMap theSearchParameterMap) {
if (myStorageSettings.isAllowMdmExpansion()) {
final RequestDetails requestDetailsToUse =
theRequestDetails == null ? new SystemRequestDetails() : theRequestDetails;
final RequestPartitionId requestPartitionId =
myRequestPartitionHelperSvc.determineReadPartitionForRequestForSearchType(
requestDetailsToUse, requestDetailsToUse.getResourceName(), theSearchParameterMap, null);
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(paramName, orList);
expandAnyReferenceParameters(requestPartitionId, paramName, orList);
}
}
}
@ -76,7 +88,8 @@ public class MdmSearchExpandingInterceptor {
/**
* If a Parameter is a reference parameter, and it has been set to expand MDM, perform the expansion.
*/
private void expandAnyReferenceParameters(String theParamName, List<IQueryParameterType> orList) {
private void expandAnyReferenceParameters(
RequestPartitionId theRequestPartitionId, String theParamName, List<IQueryParameterType> orList) {
List<IQueryParameterType> toRemove = new ArrayList<>();
List<IQueryParameterType> toAdd = new ArrayList<>();
for (IQueryParameterType iQueryParameterType : orList) {
@ -85,13 +98,13 @@ public class MdmSearchExpandingInterceptor {
if (refParam.isMdmExpand()) {
ourLog.debug("Found a reference parameter to expand: {}", refParam);
// First, attempt to expand as a source resource.
Set<String> expandedResourceIds =
myMdmLinkExpandSvc.expandMdmBySourceResourceId(new IdDt(refParam.getValue()));
Set<String> expandedResourceIds = myMdmLinkExpandSvc.expandMdmBySourceResourceId(
theRequestPartitionId, new IdDt(refParam.getValue()));
// If we failed, attempt to expand as a golden resource
if (expandedResourceIds.isEmpty()) {
expandedResourceIds =
myMdmLinkExpandSvc.expandMdmByGoldenResourceId(new IdDt(refParam.getValue()));
expandedResourceIds = myMdmLinkExpandSvc.expandMdmByGoldenResourceId(
theRequestPartitionId, new IdDt(refParam.getValue()));
}
// Rebuild the search param list.
@ -105,7 +118,7 @@ public class MdmSearchExpandingInterceptor {
}
}
} else if (theParamName.equalsIgnoreCase("_id")) {
expandIdParameter(iQueryParameterType, toAdd, toRemove);
expandIdParameter(theRequestPartitionId, iQueryParameterType, toAdd, toRemove);
}
}
@ -125,11 +138,13 @@ public class MdmSearchExpandingInterceptor {
* 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) {
@ -157,10 +172,10 @@ public class MdmSearchExpandingInterceptor {
} else if (mdmExpand) {
ourLog.debug("_id parameter must be expanded out from: {}", id.getValue());
Set<String> expandedResourceIds = myMdmLinkExpandSvc.expandMdmBySourceResourceId(id);
Set<String> expandedResourceIds = myMdmLinkExpandSvc.expandMdmBySourceResourceId(theRequestPartitionId, id);
if (expandedResourceIds.isEmpty()) {
expandedResourceIds = myMdmLinkExpandSvc.expandMdmByGoldenResourceId((IdDt) id);
expandedResourceIds = myMdmLinkExpandSvc.expandMdmByGoldenResourceId(theRequestPartitionId, (IdDt) id);
}
// Rebuild

View File

@ -20,25 +20,83 @@
package ca.uhn.fhir.mdm.model;
import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
import jakarta.annotation.Nullable;
import java.util.Objects;
import java.util.StringJoiner;
public class MdmPidTuple<T extends IResourcePersistentId> {
private final T myGoldenPid;
@Nullable
private final Integer myGoldenPartitionId;
private final T mySourcePid;
private MdmPidTuple(T theGoldenPid, T theSourcePid) {
@Nullable
private final Integer mySourcePartitionId;
private MdmPidTuple(
T theGoldenPid,
@Nullable Integer theGoldenPartitionId,
T theSourcePid,
@Nullable Integer theSourcePartitionId) {
myGoldenPid = theGoldenPid;
mySourcePid = theSourcePid;
myGoldenPartitionId = theGoldenPartitionId;
mySourcePartitionId = theSourcePartitionId;
}
public static <P extends IResourcePersistentId> MdmPidTuple<P> fromGoldenAndSource(P theGoldenPid, P theSourcePid) {
return new MdmPidTuple<>(theGoldenPid, theSourcePid);
return new MdmPidTuple<>(theGoldenPid, null, theSourcePid, null);
}
public static <P extends IResourcePersistentId> MdmPidTuple<P> fromGoldenAndSourceAndPartitionIds(
P theGoldenPid, Integer theGoldenPartitionId, P theSourcePid, Integer theSourcePartitionId) {
return new MdmPidTuple<>(theGoldenPid, theGoldenPartitionId, theSourcePid, theSourcePartitionId);
}
public T getGoldenPid() {
return myGoldenPid;
}
@Nullable
public Integer getGoldenPartitionId() {
return myGoldenPartitionId;
}
public T getSourcePid() {
return mySourcePid;
}
@Nullable
public Integer getSourcePartitionId() {
return mySourcePartitionId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MdmPidTuple<?> that = (MdmPidTuple<?>) o;
return Objects.equals(myGoldenPid, that.myGoldenPid)
&& Objects.equals(myGoldenPartitionId, that.myGoldenPartitionId)
&& Objects.equals(mySourcePid, that.mySourcePid)
&& Objects.equals(mySourcePartitionId, that.mySourcePartitionId);
}
@Override
public int hashCode() {
return Objects.hash(myGoldenPid, myGoldenPartitionId, mySourcePid, mySourcePartitionId);
}
@Override
public String toString() {
return new StringJoiner(", ", MdmPidTuple.class.getSimpleName() + "[", "]")
.add("myGoldenPid=" + myGoldenPid)
.add("myGoldenPartitionId=" + myGoldenPartitionId)
.add("mySourcePid=" + mySourcePid)
.add("mySourcePartitionId=" + mySourcePartitionId)
.toString();
}
}

View File

@ -36,9 +36,11 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashSet;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@Transactional
@ -61,88 +63,114 @@ public class MdmLinkExpandSvc implements IMdmLinkExpandSvc {
* @return A set of strings representing the FHIR IDs of the expanded resources.
*/
@Override
public Set<String> expandMdmBySourceResource(IBaseResource theResource) {
public Set<String> expandMdmBySourceResource(RequestPartitionId theRequestPartitionId, IBaseResource theResource) {
ourLog.debug("About to MDM-expand source resource {}", theResource);
return expandMdmBySourceResourceId(theResource.getIdElement());
return expandMdmBySourceResourceId(theRequestPartitionId, theResource.getIdElement());
}
/**
* Given a resource ID of a source resource, perform MDM expansion and return all the resource IDs of all resources that are
* MDM-Matched to this resource.
* Given a resource ID of a source resource, perform MDM expansion and return all the resource IDs of all resources that are
* MDM-Matched to this resource.
*
* @param theId The Resource ID of the resource to MDM-Expand
* @param theRequestPartitionId The partition ID associated with the request.
* @param theId The Resource ID of the resource to MDM-Expand
* @return A set of strings representing the FHIR ids of the expanded resources.
*/
@Override
public Set<String> expandMdmBySourceResourceId(IIdType theId) {
public Set<String> expandMdmBySourceResourceId(RequestPartitionId theRequestPartitionId, IIdType theId) {
ourLog.debug("About to expand source resource with resource id {}", theId);
return expandMdmBySourceResourcePid(
theRequestPartitionId,
myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theId));
}
/**
* Given a PID of a source resource, perform MDM expansion and return all the resource IDs of all resources that are
* MDM-Matched to this resource.
* Given a partition ID and a PID of a source resource, perform MDM expansion and return all the resource IDs of all resources that are
* MDM-Matched to this resource.
*
* @param theSourceResourcePid The PID of the resource to MDM-Expand
* @param theRequestPartitionId The partition ID associated with the request.
* @param theSourceResourcePid The PID of the resource to MDM-Expand
* @return A set of strings representing the FHIR ids of the expanded resources.
*/
@Override
public Set<String> expandMdmBySourceResourcePid(IResourcePersistentId theSourceResourcePid) {
public Set<String> expandMdmBySourceResourcePid(
RequestPartitionId theRequestPartitionId, IResourcePersistentId<?> theSourceResourcePid) {
ourLog.debug("About to expand source resource with PID {}", theSourceResourcePid);
List<MdmPidTuple> goldenPidSourcePidTuples =
final List<MdmPidTuple<?>> goldenPidSourcePidTuples =
myMdmLinkDao.expandPidsBySourcePidAndMatchResult(theSourceResourcePid, MdmMatchResultEnum.MATCH);
return flattenPidTuplesToSet(theSourceResourcePid, goldenPidSourcePidTuples);
return flattenPidTuplesToSet(theRequestPartitionId, theSourceResourcePid, goldenPidSourcePidTuples);
}
/**
* Given a PID of a golden resource, perform MDM expansion and return all the resource IDs of all resources that are
* MDM-Matched to this golden resource.
*
* @param theRequestPartitionId Partition information from the request
* @param theGoldenResourcePid The PID of the golden resource to MDM-Expand.
* @return A set of strings representing the FHIR ids of the expanded resources.
*/
@Override
public Set<String> expandMdmByGoldenResourceId(IResourcePersistentId theGoldenResourcePid) {
public Set<String> expandMdmByGoldenResourceId(
RequestPartitionId theRequestPartitionId, IResourcePersistentId<?> theGoldenResourcePid) {
ourLog.debug("About to expand golden resource with PID {}", theGoldenResourcePid);
List<MdmPidTuple> goldenPidSourcePidTuples = myMdmLinkDao.expandPidsByGoldenResourcePidAndMatchResult(
final List<MdmPidTuple<?>> goldenPidSourcePidTuples = myMdmLinkDao.expandPidsByGoldenResourcePidAndMatchResult(
theGoldenResourcePid, MdmMatchResultEnum.MATCH);
return flattenPidTuplesToSet(theGoldenResourcePid, goldenPidSourcePidTuples);
return flattenPidTuplesToSet(theRequestPartitionId, theGoldenResourcePid, goldenPidSourcePidTuples);
}
/**
* Given a resource ID of a golden resource, perform MDM expansion and return all the resource IDs of all resources that are
* MDM-Matched to this golden resource.
*
* @param theRequestPartitionId Partition information from the request
* @param theGoldenResourcePid The resource ID of the golden resource to MDM-Expand.
* @return A set of strings representing the FHIR ids of the expanded resources.
*/
@Override
public Set<String> expandMdmByGoldenResourcePid(IResourcePersistentId theGoldenResourcePid) {
public Set<String> expandMdmByGoldenResourcePid(
RequestPartitionId theRequestPartitionId, IResourcePersistentId<?> theGoldenResourcePid) {
ourLog.debug("About to expand golden resource with PID {}", theGoldenResourcePid);
List<MdmPidTuple> goldenPidSourcePidTuples = myMdmLinkDao.expandPidsByGoldenResourcePidAndMatchResult(
final List<MdmPidTuple<?>> goldenPidSourcePidTuples = myMdmLinkDao.expandPidsByGoldenResourcePidAndMatchResult(
theGoldenResourcePid, MdmMatchResultEnum.MATCH);
return flattenPidTuplesToSet(theGoldenResourcePid, goldenPidSourcePidTuples);
return flattenPidTuplesToSet(theRequestPartitionId, theGoldenResourcePid, goldenPidSourcePidTuples);
}
@Override
public Set<String> expandMdmByGoldenResourceId(IdDt theId) {
public Set<String> expandMdmByGoldenResourceId(RequestPartitionId theRequestPartitionId, IdDt theId) {
ourLog.debug("About to expand golden resource with golden resource id {}", theId);
IResourcePersistentId pidOrThrowException =
IResourcePersistentId<?> pidOrThrowException =
myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theId);
return expandMdmByGoldenResourcePid(pidOrThrowException);
return expandMdmByGoldenResourcePid(theRequestPartitionId, pidOrThrowException);
}
@Nonnull
public Set<String> flattenPidTuplesToSet(
IResourcePersistentId initialPid, List<MdmPidTuple> goldenPidSourcePidTuples) {
Set<IResourcePersistentId> flattenedPids = new HashSet<>();
goldenPidSourcePidTuples.forEach(tuple -> {
flattenedPids.add(tuple.getSourcePid());
flattenedPids.add(tuple.getGoldenPid());
});
Set<String> resourceIds = myIdHelperService.translatePidsToFhirResourceIds(flattenedPids);
ourLog.debug("Pid {} has been expanded to [{}]", initialPid, String.join(",", resourceIds));
RequestPartitionId theRequestPartitionId,
IResourcePersistentId<?> theInitialPid,
List<MdmPidTuple<?>> theGoldenPidSourcePidTuples) {
final Set<IResourcePersistentId> flattenedPids = theGoldenPidSourcePidTuples.stream()
.map(tuple -> flattenTuple(theRequestPartitionId, tuple))
.flatMap(Collection::stream)
.collect(Collectors.toUnmodifiableSet());
final Set<String> resourceIds = myIdHelperService.translatePidsToFhirResourceIds(flattenedPids);
ourLog.debug("Pid {} has been expanded to [{}]", theInitialPid, String.join(",", resourceIds));
return resourceIds;
}
@Nonnull
static Set<IResourcePersistentId> flattenTuple(RequestPartitionId theRequestPartitionId, MdmPidTuple<?> theTuple) {
if (theRequestPartitionId.isPartitionCovered(theTuple.getGoldenPartitionId())) {
if (theRequestPartitionId.isPartitionCovered(theTuple.getSourcePartitionId())) {
return Set.of(theTuple.getSourcePid(), theTuple.getGoldenPid());
}
return Set.of(theTuple.getGoldenPid());
}
if (theRequestPartitionId.isPartitionCovered(theTuple.getSourcePartitionId())) {
return Set.of(theTuple.getSourcePid());
}
return Collections.emptySet();
}
}

View File

@ -0,0 +1,178 @@
package ca.uhn.fhir.mdm.svc;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.mdm.dao.IMdmLinkDao;
import ca.uhn.fhir.mdm.model.MdmPidTuple;
import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
import jakarta.annotation.Nonnull;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.stubbing.Answer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class MdmLinkExpandSvcTest {
private static final Logger ourLog = LoggerFactory.getLogger(MdmLinkExpandSvcTest.class);
private static final int PARTITION_A = 1;
private static final int PARTITION_B = 2;
private static final int PARTITION_GOLDEN = 3;
private static final JpaPid JPA_PID_PARTITION_A_1 = JpaPid.fromId(123L);
private static final JpaPid JPA_PID_PARTITION_B = JpaPid.fromId(456L);
private static final JpaPid JPA_PID_PARTITION_A_2 = JpaPid.fromId(789L);
private static final JpaPid JPA_PID_PARTITION_DEFAULT = JpaPid.fromId(111L);
private static final JpaPid JPA_PID_PARTITION_GOLDEN = JpaPid.fromId(999L);
private static final Set<JpaPid> ALL_PIDS = Set.of(JPA_PID_PARTITION_A_1, JPA_PID_PARTITION_B, JPA_PID_PARTITION_A_2, JPA_PID_PARTITION_GOLDEN, JPA_PID_PARTITION_DEFAULT);
private static final MdmPidTuple<JpaPid> JPA_PID_MDM_PID_TUPLE_1 = MdmPidTuple.fromGoldenAndSourceAndPartitionIds(JPA_PID_PARTITION_GOLDEN, PARTITION_GOLDEN, JPA_PID_PARTITION_A_1, PARTITION_A);
private static final MdmPidTuple<JpaPid> JPA_PID_MDM_PID_TUPLE_2 = MdmPidTuple.fromGoldenAndSourceAndPartitionIds(JPA_PID_PARTITION_GOLDEN, PARTITION_GOLDEN, JPA_PID_PARTITION_B, PARTITION_B);
private static final MdmPidTuple<JpaPid> JPA_PID_MDM_PID_TUPLE_3 = MdmPidTuple.fromGoldenAndSourceAndPartitionIds(JPA_PID_PARTITION_GOLDEN, PARTITION_GOLDEN, JPA_PID_PARTITION_A_2, PARTITION_A);
private static final MdmPidTuple<JpaPid> JPA_PID_MDM_PID_TUPLE_4 = MdmPidTuple.fromGoldenAndSourceAndPartitionIds(JPA_PID_PARTITION_GOLDEN, PARTITION_GOLDEN, JPA_PID_PARTITION_DEFAULT, null);
@Mock
private IMdmLinkDao<JpaPid,?> myIMdmLinkDao;
@Mock
private IIdHelperService<?> myIdHelperService;
@InjectMocks
private MdmLinkExpandSvc mySubject;
void beforeEachExpand() {
final Answer<Set<String>> answer = invocation -> {
final Set<IResourcePersistentId<?>> param = invocation.getArgument(0);
return param.stream()
.filter(JpaPid.class::isInstance)
.map(JpaPid.class::cast)
.map(pid -> Long.toString(pid.getId()))
.collect(Collectors.toUnmodifiableSet());
};
when(myIdHelperService.translatePidsToFhirResourceIds(any()))
.thenAnswer(answer);
}
private static Stream<Arguments> partitionsAndExpectedPids() {
return Stream.of(
Arguments.of(RequestPartitionId.allPartitions(), ALL_PIDS),
Arguments.of(RequestPartitionId.defaultPartition(), Collections.singleton(JPA_PID_PARTITION_DEFAULT)),
Arguments.of(RequestPartitionId.fromPartitionIds(List.of(PARTITION_A)), Set.of(JPA_PID_PARTITION_A_1, JPA_PID_PARTITION_A_2)),
Arguments.of(RequestPartitionId.fromPartitionIds(List.of(PARTITION_B)), Collections.singleton(JPA_PID_PARTITION_B)),
Arguments.of(RequestPartitionId.fromPartitionIds(List.of(PARTITION_GOLDEN)), Collections.singleton(JPA_PID_PARTITION_GOLDEN)),
Arguments.of(RequestPartitionId.fromPartitionIds(List.of(PARTITION_A, PARTITION_B)), Set.of(JPA_PID_PARTITION_A_1, JPA_PID_PARTITION_A_2, JPA_PID_PARTITION_B)),
Arguments.of(RequestPartitionId.fromPartitionIds(Arrays.asList(PARTITION_A, PARTITION_B, null)), Set.of(JPA_PID_PARTITION_A_1, JPA_PID_PARTITION_A_2, JPA_PID_PARTITION_B, JPA_PID_PARTITION_DEFAULT)),
Arguments.of(RequestPartitionId.fromPartitionIds(Arrays.asList(PARTITION_A, PARTITION_B, PARTITION_GOLDEN, null)), ALL_PIDS)
);
}
@ParameterizedTest
@MethodSource("partitionsAndExpectedPids")
void expandMdmBySourceResourcePid(RequestPartitionId theRequestPartitionId, Set<JpaPid> theExpectedJpaPids) {
beforeEachExpand();
final JpaPid jpaPid = JpaPid.fromId(123L);
when(myIMdmLinkDao.expandPidsBySourcePidAndMatchResult(jpaPid, MdmMatchResultEnum.MATCH)).thenReturn(List.of(
JPA_PID_MDM_PID_TUPLE_1,
JPA_PID_MDM_PID_TUPLE_1,
JPA_PID_MDM_PID_TUPLE_1,
JPA_PID_MDM_PID_TUPLE_1,
JPA_PID_MDM_PID_TUPLE_2,
JPA_PID_MDM_PID_TUPLE_2,
JPA_PID_MDM_PID_TUPLE_2,
JPA_PID_MDM_PID_TUPLE_2,
JPA_PID_MDM_PID_TUPLE_3,
JPA_PID_MDM_PID_TUPLE_3,
JPA_PID_MDM_PID_TUPLE_3,
JPA_PID_MDM_PID_TUPLE_3,
JPA_PID_MDM_PID_TUPLE_3,
JPA_PID_MDM_PID_TUPLE_4,
JPA_PID_MDM_PID_TUPLE_4,
JPA_PID_MDM_PID_TUPLE_4,
JPA_PID_MDM_PID_TUPLE_4)
);
final Set<String> resolvedPids = mySubject.expandMdmBySourceResourcePid(theRequestPartitionId, jpaPid);
assertEquals(toPidStrings(theExpectedJpaPids), resolvedPids, String.format("expected: %s, actual: %s", theExpectedJpaPids, resolvedPids));
}
@ParameterizedTest
@MethodSource("partitionsAndExpectedPids")
void expandMdmByGoldenResourcePid(RequestPartitionId theRequestPartitionId, Set<JpaPid> theExpectedJpaPids) {
beforeEachExpand();
when(myIMdmLinkDao.expandPidsByGoldenResourcePidAndMatchResult(any(), any()))
.thenReturn(List.of(JPA_PID_MDM_PID_TUPLE_1, JPA_PID_MDM_PID_TUPLE_2, JPA_PID_MDM_PID_TUPLE_3, JPA_PID_MDM_PID_TUPLE_4));
final JpaPid jpaPid = JpaPid.fromId(123L);
final Set<String> resolvedPids = mySubject.expandMdmByGoldenResourcePid(theRequestPartitionId, jpaPid);
assertEquals(toPidStrings(theExpectedJpaPids), resolvedPids, String.format("expected: %s, actual: %s", theExpectedJpaPids, resolvedPids));
}
private static Stream<Arguments> partitionsAndTuples() {
return Stream.of(
Arguments.of(RequestPartitionId.allPartitions(), JPA_PID_MDM_PID_TUPLE_1, Set.of(JPA_PID_PARTITION_GOLDEN, JPA_PID_PARTITION_A_1)),
Arguments.of(RequestPartitionId.defaultPartition(), JPA_PID_MDM_PID_TUPLE_1, Collections.emptySet()),
Arguments.of(RequestPartitionId.fromPartitionIds(List.of(PARTITION_A)), JPA_PID_MDM_PID_TUPLE_1, Collections.singleton(JPA_PID_PARTITION_A_1)),
Arguments.of(RequestPartitionId.fromPartitionIds(List.of(PARTITION_B)), JPA_PID_MDM_PID_TUPLE_1, Collections.emptySet()),
Arguments.of(RequestPartitionId.fromPartitionIds(List.of(PARTITION_GOLDEN)), JPA_PID_MDM_PID_TUPLE_1, Collections.singleton(JPA_PID_PARTITION_GOLDEN)),
Arguments.of(RequestPartitionId.fromPartitionIds(List.of(PARTITION_A, PARTITION_B)), JPA_PID_MDM_PID_TUPLE_1, Collections.singleton(JPA_PID_PARTITION_A_1)),
Arguments.of(RequestPartitionId.fromPartitionIds(Arrays.asList(PARTITION_A, PARTITION_B, null)), JPA_PID_MDM_PID_TUPLE_1, Collections.singleton(JPA_PID_PARTITION_A_1)),
Arguments.of(RequestPartitionId.fromPartitionIds(Arrays.asList(PARTITION_A, PARTITION_B, PARTITION_GOLDEN, null)), JPA_PID_MDM_PID_TUPLE_1, Set.of(JPA_PID_PARTITION_GOLDEN, JPA_PID_PARTITION_A_1)),
Arguments.of(RequestPartitionId.allPartitions(), JPA_PID_MDM_PID_TUPLE_2, Set.of(JPA_PID_PARTITION_GOLDEN, JPA_PID_PARTITION_B)),
Arguments.of(RequestPartitionId.defaultPartition(), JPA_PID_MDM_PID_TUPLE_2, Collections.emptySet()),
Arguments.of(RequestPartitionId.fromPartitionIds(List.of(PARTITION_A)), JPA_PID_MDM_PID_TUPLE_2, Collections.emptySet()),
Arguments.of(RequestPartitionId.fromPartitionIds(List.of(PARTITION_B)), JPA_PID_MDM_PID_TUPLE_2, Collections.singleton(JPA_PID_PARTITION_B)),
Arguments.of(RequestPartitionId.fromPartitionIds(List.of(PARTITION_GOLDEN)), JPA_PID_MDM_PID_TUPLE_2, Collections.singleton(JPA_PID_PARTITION_GOLDEN)),
Arguments.of(RequestPartitionId.fromPartitionIds(List.of(PARTITION_A, PARTITION_B)), JPA_PID_MDM_PID_TUPLE_2, Collections.singleton(JPA_PID_PARTITION_B)),
Arguments.of(RequestPartitionId.fromPartitionIds(Arrays.asList(PARTITION_A, PARTITION_B, null)), JPA_PID_MDM_PID_TUPLE_2, Collections.singleton(JPA_PID_PARTITION_B)),
Arguments.of(RequestPartitionId.fromPartitionIds(Arrays.asList(PARTITION_A, PARTITION_B, PARTITION_GOLDEN, null)), JPA_PID_MDM_PID_TUPLE_2, Set.of(JPA_PID_PARTITION_GOLDEN, JPA_PID_PARTITION_B)),
Arguments.of(RequestPartitionId.allPartitions(), JPA_PID_MDM_PID_TUPLE_3, Set.of(JPA_PID_PARTITION_GOLDEN, JPA_PID_PARTITION_A_2)),
Arguments.of(RequestPartitionId.defaultPartition(), JPA_PID_MDM_PID_TUPLE_3, Collections.emptySet()),
Arguments.of(RequestPartitionId.fromPartitionIds(List.of(PARTITION_A)), JPA_PID_MDM_PID_TUPLE_3, Collections.singleton(JPA_PID_PARTITION_A_2)),
Arguments.of(RequestPartitionId.fromPartitionIds(List.of(PARTITION_B)), JPA_PID_MDM_PID_TUPLE_3, Collections.emptySet()),
Arguments.of(RequestPartitionId.fromPartitionIds(List.of(PARTITION_GOLDEN)), JPA_PID_MDM_PID_TUPLE_3, Collections.singleton(JPA_PID_PARTITION_GOLDEN)),
Arguments.of(RequestPartitionId.fromPartitionIds(List.of(PARTITION_A, PARTITION_B)), JPA_PID_MDM_PID_TUPLE_3, Collections.singleton(JPA_PID_PARTITION_A_2)),
Arguments.of(RequestPartitionId.fromPartitionIds(Arrays.asList(PARTITION_A, PARTITION_B, null)), JPA_PID_MDM_PID_TUPLE_3, Collections.singleton(JPA_PID_PARTITION_A_2)),
Arguments.of(RequestPartitionId.fromPartitionIds(Arrays.asList(PARTITION_A, PARTITION_B, PARTITION_GOLDEN, null)), JPA_PID_MDM_PID_TUPLE_3, Set.of(JPA_PID_PARTITION_GOLDEN, JPA_PID_PARTITION_A_2)),
Arguments.of(RequestPartitionId.allPartitions(), JPA_PID_MDM_PID_TUPLE_4, Set.of(JPA_PID_PARTITION_GOLDEN, JPA_PID_PARTITION_DEFAULT)),
Arguments.of(RequestPartitionId.defaultPartition(), JPA_PID_MDM_PID_TUPLE_4, Collections.singleton(JPA_PID_PARTITION_DEFAULT)),
Arguments.of(RequestPartitionId.fromPartitionIds(List.of(PARTITION_A)), JPA_PID_MDM_PID_TUPLE_4, Collections.emptySet()),
Arguments.of(RequestPartitionId.fromPartitionIds(List.of(PARTITION_B)), JPA_PID_MDM_PID_TUPLE_4, Collections.emptySet()),
Arguments.of(RequestPartitionId.fromPartitionIds(List.of(PARTITION_GOLDEN)), JPA_PID_MDM_PID_TUPLE_4, Collections.singleton(JPA_PID_PARTITION_GOLDEN)),
Arguments.of(RequestPartitionId.fromPartitionIds(List.of(PARTITION_A, PARTITION_B)), JPA_PID_MDM_PID_TUPLE_4, Collections.emptySet()),
Arguments.of(RequestPartitionId.fromPartitionIds(Arrays.asList(PARTITION_A, PARTITION_B, null)), JPA_PID_MDM_PID_TUPLE_4, Collections.singleton(JPA_PID_PARTITION_DEFAULT)),
Arguments.of(RequestPartitionId.fromPartitionIds(Arrays.asList(PARTITION_A, PARTITION_B, PARTITION_GOLDEN, null)), JPA_PID_MDM_PID_TUPLE_4, Set.of(JPA_PID_PARTITION_DEFAULT, JPA_PID_PARTITION_GOLDEN))
);
}
@ParameterizedTest
@MethodSource("partitionsAndTuples")
void flattenTuple(RequestPartitionId theRequestPartitionId, MdmPidTuple<JpaPid> theTuple, Set<JpaPid> theExpectedResourceIds) {
assertEquals(theExpectedResourceIds, MdmLinkExpandSvc.flattenTuple(theRequestPartitionId, theTuple));
}
@Nonnull
private Set<String> toPidStrings(Set<JpaPid> theTheExpectedJpaPids) {
return theTheExpectedJpaPids.stream().map(JpaPid::getId).map(id -> Long.toString(id)).collect(Collectors.toUnmodifiableSet());
}
}

View File

@ -62,6 +62,10 @@ public final class PatientEverythingParameters {
"Filter the resources to return only resources matching the given _type filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)")
private StringAndListParam myTypes;
@Description(
shortDefinition = "If set to true, trigger an MDM expansion of identifiers corresponding to the resources.")
private boolean myMdmExpand = false;
public IPrimitiveType<Integer> getCount() {
return myCount;
}
@ -94,6 +98,10 @@ public final class PatientEverythingParameters {
return myTypes;
}
public boolean getMdmExpand() {
return myMdmExpand;
}
public void setCount(IPrimitiveType<Integer> theCount) {
this.myCount = theCount;
}
@ -125,4 +133,8 @@ public final class PatientEverythingParameters {
public void setTypes(StringAndListParam theTypes) {
this.myTypes = theTypes;
}
public void setMdmExpand(Boolean myMdmExpand) {
this.myMdmExpand = myMdmExpand;
}
}