diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5493-linked-resources-for-several-partitions-are-not-returned-in-the-everything-operation.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5493-linked-resources-for-several-partitions-are-not-returned-in-the-everything-operation.yaml new file mode 100644 index 00000000000..dfe811b38c3 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5493-linked-resources-for-several-partitions-are-not-returned-in-the-everything-operation.yaml @@ -0,0 +1,6 @@ +--- +type: fix +jira: SMILE-7624 +title: "Previously, it was impossible to find all resources from different partitions for $everything operation + with partitioning.cross_partition_reference_mode=ALLOWED_UNQUALIFIED and dao_config.client_id_mode=ANY. + It's fixed now" \ No newline at end of file diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java index e23720ad33d..536a8aad0f5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java @@ -210,7 +210,9 @@ public class IdHelperService implements IIdHelperService { Validate.isTrue(!theIds.isEmpty(), "theIds must not be empty"); Map retVals = new HashMap<>(); - + RequestPartitionId partitionId = myPartitionSettings.isAllowUnqualifiedCrossPartitionReference() + ? RequestPartitionId.allPartitions() + : theRequestPartitionId; for (String id : theIds) { JpaPid retVal; if (!idRequiresForcedId(id)) { @@ -221,18 +223,17 @@ public class IdHelperService implements IIdHelperService { // is a forced id // we must resolve! if (myStorageSettings.isDeleteEnabled()) { - retVal = resolveResourceIdentity(theRequestPartitionId, theResourceType, id, theExcludeDeleted) + retVal = resolveResourceIdentity(partitionId, theResourceType, id, theExcludeDeleted) .getPersistentId(); retVals.put(id, retVal); } else { // fetch from cache... adding to cache if not available - String key = toForcedIdToPidKey(theRequestPartitionId, theResourceType, id); + String key = toForcedIdToPidKey(partitionId, theResourceType, id); retVal = myMemoryCacheService.getThenPutAfterCommit( MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key, t -> { List ids = Collections.singletonList(new IdType(theResourceType, id)); // fetches from cache using a function that checks cache first... - List resolvedIds = - resolveResourcePersistentIdsWithCache(theRequestPartitionId, ids); + List resolvedIds = resolveResourcePersistentIdsWithCache(partitionId, ids); if (resolvedIds.isEmpty()) { throw new ResourceNotFoundException(Msg.code(1100) + ids.get(0)); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/index/IdHelperServiceTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/index/IdHelperServiceTest.java new file mode 100644 index 00000000000..12888335a95 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/index/IdHelperServiceTest.java @@ -0,0 +1,168 @@ +package ca.uhn.fhir.jpa.dao.index; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; +import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; +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 jakarta.persistence.EntityManager; +import jakarta.persistence.Tuple; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.*; +import org.hl7.fhir.r4.hapi.ctx.FhirR4; +import org.hl7.fhir.r4.model.Patient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatchers; +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; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class IdHelperServiceTest { + + @InjectMocks + private final IdHelperService subject = new IdHelperService(); + + @Mock + protected IResourceTableDao myResourceTableDao; + + @Mock + private JpaStorageSettings myStorageSettings; + + @Mock + private FhirContext myFhirCtx; + + @Mock + private MemoryCacheService myMemoryCacheService; + + @Mock + private EntityManager myEntityManager; + + @Mock + private PartitionSettings myPartitionSettings; + + @BeforeEach + void setUp() { + subject.setDontCheckActiveTransactionForUnitTest(true); + + when(myStorageSettings.isDeleteEnabled()).thenReturn(true); + when(myStorageSettings.getResourceClientIdStrategy()).thenReturn(JpaStorageSettings.ClientIdStrategyEnum.ANY); + when(myPartitionSettings.isAllowUnqualifiedCrossPartitionReference()).thenReturn(true); + } + + @Test + public void testResolveResourcePersistentIds() { + //prepare params + RequestPartitionId requestPartitionId = RequestPartitionId.fromPartitionName("Partition-A"); + String resourceType = "Patient"; + Long id = 123L; + List ids = List.of(String.valueOf(id)); + boolean theExcludeDeleted = false; + + //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)); + + Map actualIds = subject.resolveResourcePersistentIds(requestPartitionId, resourceType, ids, theExcludeDeleted); + + //verify results + assertFalse(actualIds.isEmpty()); + assertEquals(id, actualIds.get(ids.get(0)).getId()); + } + + @Test + public void testResolveResourcePersistentIdsDeleteFalse() { + //prepare Params + RequestPartitionId requestPartitionId = RequestPartitionId.fromPartitionName("Partition-A"); + Long id = 123L; + String resourceType = "Patient"; + List ids = List.of(String.valueOf(id)); + String forcedId = "(all)/" + resourceType + "/" + id; + boolean theExcludeDeleted = false; + + //prepare results + Patient expectedPatient = new Patient(); + 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 actualIds = subject.resolveResourcePersistentIds(requestPartitionId, resourceType, ids, theExcludeDeleted); + + //verifyResult + assertFalse(actualIds.isEmpty()); + assertEquals(id, actualIds.get(ids.get(0)).getId()); + } + + 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.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 mockedTupleList = getMockedTupleList(idNumber, resourceType, id); + CriteriaBuilder builder = getMockedCriteriaBuilder(); + Root from = getMockedFrom(); + + @SuppressWarnings("unchecked") + TypedQuery query = (TypedQuery) mock(TypedQuery.class); + @SuppressWarnings("unchecked") + CriteriaQuery cq = mock(CriteriaQuery.class); + + when(builder.createTupleQuery()).thenReturn(cq); + when(cq.from(ArgumentMatchers.>any())).thenReturn(from); + when(query.getResultList()).thenReturn(mockedTupleList); + + when(myEntityManager.getCriteriaBuilder()).thenReturn(builder); + when(myEntityManager.createQuery(ArgumentMatchers.>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 getMockedFrom() { + @SuppressWarnings("unchecked") + Path path = mock(Path.class); + @SuppressWarnings("unchecked") + Root from = mock(Root.class); + lenient().when(from.get(ArgumentMatchers.any())).thenReturn(path); + return from; + } + + private List getMockedTupleList(Long idNumber, String resourceType, String id) { + Tuple tuple = mock(Tuple.class); + when(tuple.get(eq(0), eq(Long.class))).thenReturn(idNumber); + when(tuple.get(eq(1), eq(String.class))).thenReturn(resourceType); + when(tuple.get(eq(2), eq(String.class))).thenReturn(id); + return List.of(tuple); + } +}