package ca.uhn.fhir.jpa.search; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.eq; import static org.mockito.Matchers.same; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.*; import javax.persistence.EntityManager; import org.hl7.fhir.instance.model.api.IBaseResource; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.runners.MockitoJUnitRunner; import org.mockito.stubbing.Answer; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.transaction.PlatformTransactionManager; import com.google.common.collect.Lists; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.IDao; import ca.uhn.fhir.jpa.dao.ISearchBuilder; import ca.uhn.fhir.jpa.dao.SearchParameterMap; import ca.uhn.fhir.jpa.dao.data.ISearchDao; import ca.uhn.fhir.jpa.dao.data.ISearchIncludeDao; import ca.uhn.fhir.jpa.dao.data.ISearchResultDao; import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.entity.SearchResult; import ca.uhn.fhir.jpa.entity.SearchStatusEnum; import ca.uhn.fhir.jpa.entity.SearchTypeEnum; import ca.uhn.fhir.model.dstu2.resource.Patient; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.server.IBundleProvider; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.TestUtil; @SuppressWarnings({ "unchecked" }) @RunWith(MockitoJUnitRunner.class) public class SearchCoordinatorSvcImplTest { private static FhirContext ourCtx = FhirContext.forDstu3(); @Mock private IDao myCallingDao; @Mock private EntityManager myEntityManager; private int myExpectedNumberOfSearchBuildersCreated = 2; @Mock private ISearchBuilder mySearchBuider; @Mock private ISearchDao mySearchDao; @Mock private ISearchIncludeDao mySearchIncludeDao; @Mock private ISearchResultDao mySearchResultDao; @Captor ArgumentCaptor<Iterable<SearchResult>> mySearchResultIterCaptor; private SearchCoordinatorSvcImpl mySvc; @Mock private PlatformTransactionManager myTxManager; private DaoConfig myDaoConfig; @After public void after() { verify(myCallingDao, atMost(myExpectedNumberOfSearchBuildersCreated)).newSearchBuilder(); } @Before public void before() { mySvc = new SearchCoordinatorSvcImpl(); mySvc.setEntityManagerForUnitTest(myEntityManager); mySvc.setTransactionManagerForUnitTest(myTxManager); mySvc.setContextForUnitTest(ourCtx); mySvc.setSearchDaoForUnitTest(mySearchDao); mySvc.setSearchDaoIncludeForUnitTest(mySearchIncludeDao); mySvc.setSearchDaoResultForUnitTest(mySearchResultDao); myDaoConfig = new DaoConfig(); mySvc.setDaoConfigForUnitTest(myDaoConfig); when(myCallingDao.newSearchBuilder()).thenReturn(mySearchBuider); doAnswer(new Answer<Void>() { @Override public Void answer(InvocationOnMock theInvocation) throws Throwable { PersistedJpaBundleProvider provider = (PersistedJpaBundleProvider) theInvocation.getArguments()[0]; provider.setSearchCoordinatorSvc(mySvc); provider.setPlatformTransactionManager(myTxManager); provider.setSearchDao(mySearchDao); provider.setEntityManager(myEntityManager); provider.setContext(ourCtx); return null; }}).when(myCallingDao).injectDependenciesIntoBundleProvider(any(PersistedJpaBundleProvider.class)); } private List<Long> createPidSequence(int from, int to) { List<Long> pids = new ArrayList<Long>(); for (long i = from; i < to; i++) { pids.add(i); } return pids; } private Answer<Void> loadPids() { Answer<Void> retVal = new Answer<Void>() { @Override public Void answer(InvocationOnMock theInvocation) throws Throwable { List<Long> pids = (List<Long>) theInvocation.getArguments()[0]; List<IBaseResource> resources = (List<IBaseResource>) theInvocation.getArguments()[1]; for (Long nextPid : pids) { Patient pt = new Patient(); pt.setId(nextPid.toString()); resources.add(pt); } return null; } }; return retVal; } @Test public void testAsyncSearchFailDuringSearchSameCoordinator() { SearchParameterMap params = new SearchParameterMap(); params.add("name", new StringParam("ANAME")); List<Long> pids = createPidSequence(10, 800); Iterator<Long> iter = new FailAfterNIterator<Long>(new SlowIterator<Long>(pids.iterator(), 2), 300); when(mySearchBuider.createQuery(Mockito.same(params))).thenReturn(iter); doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(List.class), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao)); IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient"); assertNotNull(result.getUuid()); assertEquals(null, result.size()); try { result.getResources(0, 100000); } catch (InternalErrorException e) { assertEquals("FAILED", e.getMessage()); } } @Test public void testAsyncSearchLargeResultSetBigCountSameCoordinator() { SearchParameterMap params = new SearchParameterMap(); params.add("name", new StringParam("ANAME")); List<Long> pids = createPidSequence(10, 800); Iterator<Long> iter = new SlowIterator<Long>(pids.iterator(), 2); when(mySearchBuider.createQuery(Mockito.same(params))).thenReturn(iter); doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(List.class), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao)); IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient"); assertNotNull(result.getUuid()); assertEquals(null, result.size()); List<IBaseResource> resources; resources = result.getResources(0, 100000); assertEquals(790, resources.size()); assertEquals("10", resources.get(0).getIdElement().getValueAsString()); assertEquals("799", resources.get(789).getIdElement().getValueAsString()); ArgumentCaptor<Search> searchCaptor = ArgumentCaptor.forClass(Search.class); verify(mySearchDao, atLeastOnce()).save(searchCaptor.capture()); verify(mySearchResultDao, atLeastOnce()).save(mySearchResultIterCaptor.capture()); List<SearchResult> allResults= new ArrayList<SearchResult>(); for (Iterable<SearchResult> next : mySearchResultIterCaptor.getAllValues()) { allResults.addAll(Lists.newArrayList(next)); } assertEquals(790, allResults.size()); assertEquals(10, allResults.get(0).getResourcePid().longValue()); assertEquals(799, allResults.get(789).getResourcePid().longValue()); } @Test public void testAsyncSearchLargeResultSetSameCoordinator() { SearchParameterMap params = new SearchParameterMap(); params.add("name", new StringParam("ANAME")); List<Long> pids = createPidSequence(10, 800); SlowIterator<Long> iter = new SlowIterator<Long>(pids.iterator(), 2); when(mySearchBuider.createQuery(Mockito.same(params))).thenReturn(iter); doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(List.class), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao)); IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient"); assertNotNull(result.getUuid()); assertEquals(null, result.size()); List<IBaseResource> resources; resources = result.getResources(0, 30); assertEquals(30, resources.size()); assertEquals("10", resources.get(0).getIdElement().getValueAsString()); assertEquals("39", resources.get(29).getIdElement().getValueAsString()); } /** * Subsequent requests for the same search (i.e. a request for the next * page) within the same JVM will not use the original bundle provider */ @Test public void testAsyncSearchLargeResultSetSecondRequestSameCoordinator() { SearchParameterMap params = new SearchParameterMap(); params.add("name", new StringParam("ANAME")); List<Long> pids = createPidSequence(10, 800); Iterator<Long> iter = new SlowIterator<Long>(pids.iterator(), 2); when(mySearchBuider.createQuery(Mockito.same(params))).thenReturn(iter); doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(List.class), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao)); IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient"); assertNotNull(result.getUuid()); assertEquals(null, result.size()); ArgumentCaptor<Search> searchCaptor = ArgumentCaptor.forClass(Search.class); verify(mySearchDao, atLeast(1)).save(searchCaptor.capture()); Search search = searchCaptor.getValue(); assertEquals(SearchTypeEnum.SEARCH, search.getSearchType()); List<IBaseResource> resources; PersistedJpaBundleProvider provider; resources = result.getResources(0, 10); assertNull(result.size()); assertEquals(10, resources.size()); assertEquals("10", resources.get(0).getIdElement().getValueAsString()); assertEquals("19", resources.get(9).getIdElement().getValueAsString()); when(mySearchDao.findByUuid(eq(result.getUuid()))).thenReturn(search); /* * Now call from a new bundle provider. This simulates a separate HTTP * client request coming in. */ provider = new PersistedJpaBundleProvider(result.getUuid(), myCallingDao); resources = provider.getResources(10, 20); assertEquals(10, resources.size()); assertEquals("20", resources.get(0).getIdElement().getValueAsString()); assertEquals("29", resources.get(9).getIdElement().getValueAsString()); provider = new PersistedJpaBundleProvider(result.getUuid(), myCallingDao); resources = provider.getResources(20, 99999); assertEquals(770, resources.size()); assertEquals("30", resources.get(0).getIdElement().getValueAsString()); assertEquals("799", resources.get(769).getIdElement().getValueAsString()); myExpectedNumberOfSearchBuildersCreated = 4; } @Test public void testAsyncSearchSmallResultSetSameCoordinator() { SearchParameterMap params = new SearchParameterMap(); params.add("name", new StringParam("ANAME")); List<Long> pids = createPidSequence(10, 100); SlowIterator<Long> iter = new SlowIterator<Long>(pids.iterator(), 2); when(mySearchBuider.createQuery(Mockito.same(params))).thenReturn(iter); doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(List.class), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao)); IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient"); assertNotNull(result.getUuid()); assertEquals(90, result.size().intValue()); List<IBaseResource> resources = result.getResources(0, 30); assertEquals(30, resources.size()); assertEquals("10", resources.get(0).getIdElement().getValueAsString()); assertEquals("39", resources.get(29).getIdElement().getValueAsString()); } @Test public void testGetPage() { Pageable page = SearchCoordinatorSvcImpl.toPage(50, 73); assertEquals(50, page.getOffset()); } @Test public void testLoadSearchResultsFromDifferentCoordinator() { final String uuid = UUID.randomUUID().toString(); final Search search = new Search(); search.setUuid(uuid); search.setSearchType(SearchTypeEnum.SEARCH); search.setResourceType("Patient"); when(mySearchDao.findByUuid(eq(uuid))).thenReturn(search); doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(List.class), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao)); PersistedJpaBundleProvider provider; List<IBaseResource> resources; new Thread() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { // ignore } when(mySearchResultDao.findWithSearchUuid(any(Search.class), any(Pageable.class))).thenAnswer(new Answer<Page<SearchResult>>() { @Override public Page<SearchResult> answer(InvocationOnMock theInvocation) throws Throwable { Pageable page = (Pageable) theInvocation.getArguments()[1]; ArrayList<SearchResult> results = new ArrayList<SearchResult>(); int max = (page.getPageNumber() * page.getPageSize()) + page.getPageSize(); for (int i = page.getOffset(); i < max; i++) { results.add(new SearchResult().setResourcePid(i + 10L)); } return new PageImpl<SearchResult>(results); }}); search.setStatus(SearchStatusEnum.FINISHED); } }.start(); /* * Now call from a new bundle provider. This simulates a separate HTTP * client request coming in. */ provider = new PersistedJpaBundleProvider(uuid, myCallingDao); resources = provider.getResources(10, 20); assertEquals(10, resources.size()); assertEquals("20", resources.get(0).getIdElement().getValueAsString()); assertEquals("29", resources.get(9).getIdElement().getValueAsString()); provider = new PersistedJpaBundleProvider(uuid, myCallingDao); resources = provider.getResources(20, 40); assertEquals(20, resources.size()); assertEquals("30", resources.get(0).getIdElement().getValueAsString()); assertEquals("49", resources.get(19).getIdElement().getValueAsString()); myExpectedNumberOfSearchBuildersCreated = 3; } @Test public void testSynchronousSearch() { SearchParameterMap params = new SearchParameterMap(); params.setLoadSynchronous(true); params.add("name", new StringParam("ANAME")); List<Long> pids = createPidSequence(10, 800); when(mySearchBuider.createQuery(Mockito.same(params))).thenReturn(pids.iterator()); doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(eq(pids), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao)); IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient"); assertNull(result.getUuid()); assertEquals(790, result.size().intValue()); List<IBaseResource> resources = result.getResources(0, 10000); assertEquals(790, resources.size()); assertEquals("10", resources.get(0).getIdElement().getValueAsString()); assertEquals("799", resources.get(789).getIdElement().getValueAsString()); } @Test public void testSynchronousSearchUpTo() { SearchParameterMap params = new SearchParameterMap(); params.setLoadSynchronousUpTo(100); params.add("name", new StringParam("ANAME")); List<Long> pids = createPidSequence(10, 800); when(mySearchBuider.createQuery(Mockito.same(params))).thenReturn(pids.iterator()); pids = createPidSequence(10, 110); doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(eq(pids), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao)); IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient"); assertNull(result.getUuid()); assertEquals(100, result.size().intValue()); List<IBaseResource> resources = result.getResources(0, 10000); assertEquals(100, resources.size()); assertEquals("10", resources.get(0).getIdElement().getValueAsString()); assertEquals("109", resources.get(99).getIdElement().getValueAsString()); } @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); } public static class FailAfterNIterator<T> implements Iterator<T> { private int myCount; private Iterator<T> myWrap; public FailAfterNIterator(Iterator<T> theWrap, int theCount) { myWrap = theWrap; myCount = theCount; } @Override public boolean hasNext() { return myWrap.hasNext(); } @Override public T next() { myCount--; if (myCount == 0) { throw new NullPointerException("FAILED"); } return myWrap.next(); } } public static class SlowIterator<T> implements Iterator<T> { private int myDelay; private Iterator<T> myWrap; public SlowIterator(Iterator<T> theWrap, int theDelay) { myWrap = theWrap; myDelay = theDelay; } @Override public boolean hasNext() { return myWrap.hasNext(); } @Override public T next() { try { Thread.sleep(myDelay); } catch (InterruptedException e) { // ignore } return myWrap.next(); } } }