From 217d9f81767236a3f60cc073edffc7f81a85a166 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Sun, 16 Apr 2017 14:46:15 -0400 Subject: [PATCH] More perf work --- .../ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java | 3 +- .../fhir/jpa/dao/BaseHapiFhirResourceDao.java | 81 --- .../ca/uhn/fhir/jpa/dao/SearchBuilder.java | 6 +- .../jpa/search/SearchCoordinatorSvcImpl.java | 8 +- .../dstu3/ResourceProviderDstu3Test.java | 6 +- .../fhir/rest/server/Dstu1BundleFactory.java | 52 +- ...archBundleProviderWithNoSizeDstu1Test.java | 186 +++++ .../provider/dstu2/Dstu2BundleFactory.java | 16 +- ...archBundleProviderWithNoSizeDstu2Test.java | 186 +++++ .../hapi/rest/server/Dstu3BundleFactory.java | 658 +++++++++--------- ...archBundleProviderWithNoSizeDstu3Test.java | 79 ++- 11 files changed, 830 insertions(+), 451 deletions(-) create mode 100644 hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/SearchBundleProviderWithNoSizeDstu1Test.java create mode 100644 hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/SearchBundleProviderWithNoSizeDstu2Test.java diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index c34ea577282..b95aab0a0ec 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -696,7 +696,8 @@ public abstract class BaseHapiFhirDao implements IDao { search.setResourceType(resourceName); search.setResourceId(theId); search.setSearchType(SearchTypeEnum.HISTORY); - + search.setStatus(SearchStatusEnum.FINISHED); + if (theSince != null) { if (resourceName == null) { search.setTotalCount(myResourceHistoryTableDao.countForAllResourceTypes(theSince)); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index 58cf016197e..600c9aadad9 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -593,87 +593,6 @@ public abstract class BaseHapiFhirResourceDao extends B return retVal; } - // @Override - // public IBundleProvider everything(IIdType theId) { - // Search search = new Search(); - // search.setUuid(UUID.randomUUID().toString()); - // search.setCreated(new Date()); - // myEntityManager.persist(search); - // - // List results = new ArrayList(); - // if (theId != null) { - // Long pid = translateForcedIdToPid(theId); - // ResourceTable entity = myEntityManager.find(ResourceTable.class, pid); - // validateGivenIdIsAppropriateToRetrieveResource(theId, entity); - // SearchResult res = new SearchResult(search); - // res.setResourcePid(pid); - // results.add(res); - // } else { - // TypedQuery query = createSearchAllByTypeQuery(); - // for (Tuple next : query.getResultList()) { - // SearchResult res = new SearchResult(search); - // res.setResourcePid(next.get(0, Long.class)); - // results.add(res); - // } - // } - // - // int totalCount = results.size(); - // mySearchResultDao.save(results); - // mySearchResultDao.flush(); - // - // CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); - // - // // Load _revincludes - // CriteriaQuery cq = builder.createQuery(Long.class); - // Root from = cq.from(ResourceLink.class); - // cq.select(from.get("mySourceResourcePid").as(Long.class)); - // - // Subquery pidsSubquery = cq.subquery(Long.class); - // Root pidsSubqueryFrom = pidsSubquery.from(SearchResult.class); - // pidsSubquery.select(pidsSubqueryFrom.get("myResourcePid").as(Long.class)); - // pidsSubquery.where(pidsSubqueryFrom.get("mySearch").in(search)); - // - // cq.where(from.get("myTargetResourceId").in(pidsSubquery)); - // TypedQuery query = myEntityManager.createQuery(cq); - // - // results = new ArrayList(); - // for (Long next : query.getResultList()) { - // SearchResult res = new SearchResult(search); - // res.setResourcePid(next); - // results.add(res); - // } - // - // // Save _revincludes - // totalCount += results.size(); - // mySearchResultDao.save(results); - // mySearchResultDao.flush(); - // - // final int finalTotalCount = totalCount; - // return new IBundleProvider() { - // - // @Override - // public int size() { - // return finalTotalCount; - // } - // - // @Override - // public Integer preferredPageSize() { - // return null; - // } - // - // @Override - // public List getResources(int theFromIndex, int theToIndex) { - // // TODO Auto-generated method stub - // return null; - // } - // - // @Override - // public InstantDt getPublished() { - // // TODO Auto-generated method stub - // return null; - // } - // }; - // } @Override public MT metaGetOperation(Class theType, IIdType theId, RequestDetails theRequestDetails) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java index 5c5d1338b53..eaaad27a9b0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java @@ -1315,8 +1315,10 @@ public class SearchBuilder implements ISearchBuilder { @Override public Long next() { fetchNext(); - Validate.isTrue(myNext != NO_MORE, "No more elements"); - return myNext; + Long retVal = myNext; + myNext = null; + Validate.isTrue(retVal != NO_MORE, "No more elements"); + return retVal; } }; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java index 874df903809..0e2b5fc93bd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java @@ -268,6 +268,8 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { @Override public Void call() throws Exception { + StopWatch sw = new StopWatch(); + try { TransactionTemplate txTemplate = new TransactionTemplate(myTxManager); txTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); @@ -277,14 +279,18 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { doSearch(); } }); + + ourLog.info("Completed search for {} resources in {}ms", mySyncedPids.size(), sw.getMillis()); + } catch (Throwable t) { - ourLog.error("Failed during search loading", t); + ourLog.error("Failed during search loading after {}ms", t, sw.getMillis()); myUnsyncedPids.clear(); mySearch.setStatus(SearchStatusEnum.FAILED); String failureMessage = ExceptionUtils.getRootCauseMessage(t); mySearch.setFailureMessage(failureMessage); saveSearch(); + } myIdToSearchTask.remove(mySearch.getUuid()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java index c9a02541bce..a437d2db886 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java @@ -308,9 +308,6 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test { @Test public void testCountParam() throws Exception { - // NB this does not get used- The paging provider has its own limits built in - myDaoConfig.setHardSearchLimit(100); - List resources = new ArrayList(); for (int i = 0; i < 100; i++) { Organization org = new Organization(); @@ -1611,6 +1608,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test { assertEquals(77, ids.size()); } + @SuppressWarnings("unused") @Test public void testFullTextSearch() throws RuntimeException, Exception { Observation obs1 = new Observation(); @@ -1920,6 +1918,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test { } } + @SuppressWarnings("unused") @Test public void testMetadataSuperParamsAreIncluded() throws IOException { StructureDefinition p = new StructureDefinition(); @@ -2622,6 +2621,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test { } + @SuppressWarnings("unused") @Test public void testSearchPagingKeepsOldSearches() throws Exception { String methodName = "testSearchPagingKeepsOldSearches"; diff --git a/hapi-fhir-structures-dstu/src/main/java/ca/uhn/fhir/rest/server/Dstu1BundleFactory.java b/hapi-fhir-structures-dstu/src/main/java/ca/uhn/fhir/rest/server/Dstu1BundleFactory.java index 3965f0bf294..9f059ebba7b 100644 --- a/hapi-fhir-structures-dstu/src/main/java/ca/uhn/fhir/rest/server/Dstu1BundleFactory.java +++ b/hapi-fhir-structures-dstu/src/main/java/ca/uhn/fhir/rest/server/Dstu1BundleFactory.java @@ -60,6 +60,23 @@ public class Dstu1BundleFactory implements IVersionSpecificBundleFactory { myContext = theContext; } + private void addProfileIfNeeded(IRestfulServer theServer, String theServerBase, IBaseResource nextRes) { + RuntimeResourceDefinition def = theServer.getFhirContext().getResourceDefinition(nextRes); + if (theServer.getAddProfileTag() == AddProfileTagEnum.ALWAYS || !def.isStandardType()) { + TagList tl = ResourceMetadataKeyEnum.TAG_LIST.get((IResource) nextRes); + if (tl == null) { + tl = new TagList(); + ResourceMetadataKeyEnum.TAG_LIST.put((IResource) nextRes, tl); + } + + RuntimeResourceDefinition nextDef = myContext.getResourceDefinition(nextRes); + String profile = nextDef.getResourceProfile(theServerBase); + if (isNotBlank(profile)) { + tl.add(new Tag(Tag.HL7_ORG_PROFILE_TAG, profile, null)); + } + } + } + @Override public void addResourcesToBundle(List theResult, BundleTypeEnum theBundleType, String theServerBase, BundleInclusionRule theBundleInclusionRule, Set theIncludes) { if (myBundle == null) { @@ -149,7 +166,7 @@ public class Dstu1BundleFactory implements IVersionSpecificBundleFactory { myBundle.getAuthorName().setValue(theAuthor); } - if (myBundle.getUpdated().isEmpty() && isNotBlank(theLastUpdated.getValueAsString())) { + if (theLastUpdated != null && myBundle.getUpdated().isEmpty() && isNotBlank(theLastUpdated.getValueAsString())) { myBundle.getUpdated().setValueAsString(theLastUpdated.getValueAsString()); } @@ -190,27 +207,31 @@ public class Dstu1BundleFactory implements IVersionSpecificBundleFactory { int numToReturn; String searchId = null; List resourceList; + Integer numTotalResults = theResult.size(); if (theServer.getPagingProvider() == null) { - numToReturn = theResult.size(); + numToReturn = numTotalResults; resourceList = theResult.getResources(0, numToReturn); RestfulServerUtils.validateResourceListNotNull(resourceList); } else { IPagingProvider pagingProvider = theServer.getPagingProvider(); - if (theLimit == null) { + if (theLimit == null || theLimit.equals(Integer.valueOf(0))) { numToReturn = pagingProvider.getDefaultPageSize(); } else { numToReturn = Math.min(pagingProvider.getMaximumPageSize(), theLimit); } - numToReturn = Math.min(numToReturn, theResult.size() - theOffset); + if (numTotalResults != null) { + numToReturn = Math.min(numToReturn, numTotalResults - theOffset); + } + resourceList = theResult.getResources(theOffset, numToReturn + theOffset); RestfulServerUtils.validateResourceListNotNull(resourceList); if (theSearchId != null) { searchId = theSearchId; } else { - if (theResult.size() > numToReturn) { + if (numTotalResults == null || numTotalResults > numToReturn) { searchId = pagingProvider.storeResultList(theResult); Validate.notNull(searchId, "Paging provider returned null searchId"); } @@ -230,7 +251,7 @@ public class Dstu1BundleFactory implements IVersionSpecificBundleFactory { addResourcesToBundle(new ArrayList(resourceList), theBundleType, theServerBase, theServer.getBundleInclusionRule(), theIncludes); - addRootPropertiesToBundle(null, theServerBase, theCompleteUrl, theResult.size(), theBundleType, theResult.getPublished()); + addRootPropertiesToBundle(null, theServerBase, theCompleteUrl, numTotalResults, theBundleType, theResult.getPublished()); if (theServer.getPagingProvider() != null) { int limit; @@ -238,7 +259,7 @@ public class Dstu1BundleFactory implements IVersionSpecificBundleFactory { limit = Math.min(limit, theServer.getPagingProvider().getMaximumPageSize()); if (searchId != null) { - if (theOffset + numToReturn < theResult.size()) { + if (numTotalResults == null || theOffset + numToReturn < numTotalResults) { myBundle.getLinkNext() .setValue(RestfulServerUtils.createPagingLink(theIncludes, theServerBase, searchId, theOffset + numToReturn, numToReturn, theResponseEncoding, thePrettyPrint, theBundleType)); } @@ -250,23 +271,6 @@ public class Dstu1BundleFactory implements IVersionSpecificBundleFactory { } } - private void addProfileIfNeeded(IRestfulServer theServer, String theServerBase, IBaseResource nextRes) { - RuntimeResourceDefinition def = theServer.getFhirContext().getResourceDefinition(nextRes); - if (theServer.getAddProfileTag() == AddProfileTagEnum.ALWAYS || !def.isStandardType()) { - TagList tl = ResourceMetadataKeyEnum.TAG_LIST.get((IResource) nextRes); - if (tl == null) { - tl = new TagList(); - ResourceMetadataKeyEnum.TAG_LIST.put((IResource) nextRes, tl); - } - - RuntimeResourceDefinition nextDef = myContext.getResourceDefinition(nextRes); - String profile = nextDef.getResourceProfile(theServerBase); - if (isNotBlank(profile)) { - tl.add(new Tag(Tag.HL7_ORG_PROFILE_TAG, profile, null)); - } - } - } - @Override public void initializeBundleFromResourceList(String theAuthor, List theResult, String theServerBase, String theCompleteUrl, int theTotalResults, BundleTypeEnum theBundleType) { diff --git a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/SearchBundleProviderWithNoSizeDstu1Test.java b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/SearchBundleProviderWithNoSizeDstu1Test.java new file mode 100644 index 00000000000..38c6b2fc3a4 --- /dev/null +++ b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/SearchBundleProviderWithNoSizeDstu1Test.java @@ -0,0 +1,186 @@ +package ca.uhn.fhir.rest.server; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import com.google.common.collect.Lists; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.Bundle; +import ca.uhn.fhir.model.dstu.resource.Patient; +import ca.uhn.fhir.model.primitive.StringDt; +import ca.uhn.fhir.rest.annotation.RequiredParam; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.param.TokenAndListParam; +import ca.uhn.fhir.util.PortUtil; +import ca.uhn.fhir.util.TestUtil; + +public class SearchBundleProviderWithNoSizeDstu1Test { + + private static CloseableHttpClient ourClient; + private static FhirContext ourCtx = FhirContext.forDstu1(); + private static TokenAndListParam ourIdentifiers; + private static IBundleProvider ourLastBundleProvider; + private static String ourLastMethod; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchBundleProviderWithNoSizeDstu1Test.class); + private static int ourPort; + + private static Server ourServer; + + @Before + public void before() { + ourLastMethod = null; + ourIdentifiers = null; + } + + @Test + public void testBundleProviderReturnsNoSize() throws Exception { + Bundle respBundle; + + ourLastBundleProvider = mock(IBundleProvider.class); + when(ourLastBundleProvider.size()).thenReturn(null); + when(ourLastBundleProvider.getResources(any(int.class), any(int.class))).then(new Answer>() { + @Override + public List answer(InvocationOnMock theInvocation) throws Throwable { + int from =(Integer)theInvocation.getArguments()[0]; + int to =(Integer)theInvocation.getArguments()[1]; + ArrayList retVal = Lists.newArrayList(); + for (int i = from; i < to; i++) { + Patient p = new Patient(); + p.setId(Integer.toString(i)); + retVal.add(p); + } + return retVal; + }}); + + HttpGet httpGet; + CloseableHttpResponse status = null; + StringDt linkNext; + + try { + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_format=json"); + status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(responseContent); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals("searchAll", ourLastMethod); + respBundle = ourCtx.newJsonParser().parseBundle(responseContent); + + assertEquals(10, respBundle.getEntries().size()); + assertEquals("Patient/0", respBundle.getEntries().get(0).getResource().getIdElement().toUnqualifiedVersionless().getValue()); + linkNext = respBundle.getLinkNext(); + assertNotNull(linkNext.getValue()); + + } finally { + IOUtils.closeQuietly(status.getEntity().getContent()); + } + + + when(ourLastBundleProvider.size()).thenReturn(25); + + try { + httpGet = new HttpGet(linkNext.getValue()); + status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(responseContent); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals("searchAll", ourLastMethod); + respBundle = ourCtx.newJsonParser().parseBundle(responseContent); + + assertEquals(10, respBundle.getEntries().size()); + assertEquals("Patient/10", respBundle.getEntries().get(0).getResource().getIdElement().toUnqualifiedVersionless().getValue()); + linkNext = respBundle.getLinkNext(); + assertNotNull(linkNext.getValue()); + + } finally { + IOUtils.closeQuietly(status.getEntity().getContent()); + } + + try { + httpGet = new HttpGet(linkNext.getValue()); + status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(responseContent); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals("searchAll", ourLastMethod); + respBundle = ourCtx.newJsonParser().parseBundle(responseContent); + + assertEquals(5, respBundle.getEntries().size()); + assertEquals("Patient/20", respBundle.getEntries().get(0).getResource().getIdElement().toUnqualifiedVersionless().getValue()); + linkNext = respBundle.getLinkNext(); + assertNull(linkNext.getValue()); + + } finally { + IOUtils.closeQuietly(status.getEntity().getContent()); + } + + } + + @AfterClass + public static void afterClassClearContext() throws Exception { + ourServer.stop(); + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + @BeforeClass + public static void beforeClass() throws Exception { + ourPort = PortUtil.findFreePort(); + ourServer = new Server(ourPort); + + DummyPatientResourceProvider patientProvider = new DummyPatientResourceProvider(); + + ServletHandler proxyHandler = new ServletHandler(); + RestfulServer servlet = new RestfulServer(ourCtx); + servlet.setPagingProvider(new FifoMemoryPagingProvider(10)); + + servlet.setResourceProviders(patientProvider); + ServletHolder servletHolder = new ServletHolder(servlet); + proxyHandler.addServletWithMapping(servletHolder, "/*"); + ourServer.setHandler(proxyHandler); + ourServer.start(); + + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + builder.setConnectionManager(connectionManager); + ourClient = builder.build(); + + } + + public static class DummyPatientResourceProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Patient.class; + } + + @Search() + public IBundleProvider searchAll() { + ourLastMethod = "searchAll"; + return ourLastBundleProvider; + } + + } + +} diff --git a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/Dstu2BundleFactory.java b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/Dstu2BundleFactory.java index acf6052b265..95280e5d37e 100644 --- a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/Dstu2BundleFactory.java +++ b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/Dstu2BundleFactory.java @@ -306,8 +306,9 @@ public class Dstu2BundleFactory implements IVersionSpecificBundleFactory { int numToReturn; String searchId = null; List resourceList; + Integer numTotalResults = theResult.size(); if (theServer.getPagingProvider() == null) { - numToReturn = theResult.size(); + numToReturn = numTotalResults; if (numToReturn > 0) { resourceList = theResult.getResources(0, numToReturn); } else { @@ -317,13 +318,16 @@ public class Dstu2BundleFactory implements IVersionSpecificBundleFactory { } else { IPagingProvider pagingProvider = theServer.getPagingProvider(); - if (theLimit == null) { + if (theLimit == null || theLimit.equals(Integer.valueOf(0))) { numToReturn = pagingProvider.getDefaultPageSize(); } else { numToReturn = Math.min(pagingProvider.getMaximumPageSize(), theLimit); } - numToReturn = Math.min(numToReturn, theResult.size() - theOffset); + if (numTotalResults != null) { + numToReturn = Math.min(numToReturn, numTotalResults - theOffset); + } + if (numToReturn > 0) { resourceList = theResult.getResources(theOffset, numToReturn + theOffset); } else { @@ -334,7 +338,7 @@ public class Dstu2BundleFactory implements IVersionSpecificBundleFactory { if (theSearchId != null) { searchId = theSearchId; } else { - if (theResult.size() > numToReturn) { + if (numTotalResults == null || numTotalResults > numToReturn) { searchId = pagingProvider.storeResultList(theResult); Validate.notNull(searchId, "Paging provider returned null searchId"); } @@ -350,7 +354,7 @@ public class Dstu2BundleFactory implements IVersionSpecificBundleFactory { } addResourcesToBundle(new ArrayList(resourceList), theBundleType, theServerBase, theServer.getBundleInclusionRule(), theIncludes); - addRootPropertiesToBundle(null, theServerBase, theCompleteUrl, theResult.size(), theBundleType, theResult.getPublished()); + addRootPropertiesToBundle(null, theServerBase, theCompleteUrl, numTotalResults, theBundleType, theResult.getPublished()); if (theServer.getPagingProvider() != null) { int limit; @@ -358,7 +362,7 @@ public class Dstu2BundleFactory implements IVersionSpecificBundleFactory { limit = Math.min(limit, theServer.getPagingProvider().getMaximumPageSize()); if (searchId != null) { - if (theOffset + numToReturn < theResult.size()) { + if (numTotalResults == null || theOffset + numToReturn < numTotalResults) { myBundle.addLink().setRelation(Constants.LINK_NEXT) .setUrl(RestfulServerUtils.createPagingLink(theIncludes, theServerBase, searchId, theOffset + numToReturn, numToReturn, theResponseEncoding, thePrettyPrint, theBundleType)); } diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/SearchBundleProviderWithNoSizeDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/SearchBundleProviderWithNoSizeDstu2Test.java new file mode 100644 index 00000000000..b0298a62938 --- /dev/null +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/SearchBundleProviderWithNoSizeDstu2Test.java @@ -0,0 +1,186 @@ +package ca.uhn.fhir.rest.server; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import com.google.common.collect.Lists; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.dstu2.resource.Bundle; +import ca.uhn.fhir.model.dstu2.resource.Bundle.Link; +import ca.uhn.fhir.model.dstu2.resource.Patient; +import ca.uhn.fhir.rest.annotation.RequiredParam; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.param.TokenAndListParam; +import ca.uhn.fhir.util.PortUtil; +import ca.uhn.fhir.util.TestUtil; + +public class SearchBundleProviderWithNoSizeDstu2Test { + + private static CloseableHttpClient ourClient; + private static FhirContext ourCtx = FhirContext.forDstu2(); + private static TokenAndListParam ourIdentifiers; + private static IBundleProvider ourLastBundleProvider; + private static String ourLastMethod; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchBundleProviderWithNoSizeDstu2Test.class); + private static int ourPort; + + private static Server ourServer; + + @Before + public void before() { + ourLastMethod = null; + ourIdentifiers = null; + } + + @Test + public void testBundleProviderReturnsNoSize() throws Exception { + Bundle respBundle; + + ourLastBundleProvider = mock(IBundleProvider.class); + when(ourLastBundleProvider.size()).thenReturn(null); + when(ourLastBundleProvider.getResources(any(int.class), any(int.class))).then(new Answer>() { + @Override + public List answer(InvocationOnMock theInvocation) throws Throwable { + int from =(Integer)theInvocation.getArguments()[0]; + int to =(Integer)theInvocation.getArguments()[1]; + ArrayList retVal = Lists.newArrayList(); + for (int i = from; i < to; i++) { + Patient p = new Patient(); + p.setId(Integer.toString(i)); + retVal.add(p); + } + return retVal; + }}); + + HttpGet httpGet; + CloseableHttpResponse status = null; + Link linkNext; + + try { + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_format=json"); + status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(responseContent); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals("searchAll", ourLastMethod); + respBundle = ourCtx.newJsonParser().parseResource(Bundle.class, responseContent); + + assertEquals(10, respBundle.getEntry().size()); + assertEquals("Patient/0", respBundle.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless().getValue()); + linkNext = respBundle.getLink("next"); + assertNotNull(linkNext); + + } finally { + IOUtils.closeQuietly(status.getEntity().getContent()); + } + + + when(ourLastBundleProvider.size()).thenReturn(25); + + try { + httpGet = new HttpGet(linkNext.getUrl()); + status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(responseContent); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals("searchAll", ourLastMethod); + respBundle = ourCtx.newJsonParser().parseResource(Bundle.class, responseContent); + + assertEquals(10, respBundle.getEntry().size()); + assertEquals("Patient/10", respBundle.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless().getValue()); + linkNext = respBundle.getLink("next"); + assertNotNull(linkNext); + + } finally { + IOUtils.closeQuietly(status.getEntity().getContent()); + } + + try { + httpGet = new HttpGet(linkNext.getUrl()); + status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(responseContent); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals("searchAll", ourLastMethod); + respBundle = ourCtx.newJsonParser().parseResource(Bundle.class, responseContent); + + assertEquals(5, respBundle.getEntry().size()); + assertEquals("Patient/20", respBundle.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless().getValue()); + linkNext = respBundle.getLink("next"); + assertNull(linkNext); + + } finally { + IOUtils.closeQuietly(status.getEntity().getContent()); + } + + } + + @AfterClass + public static void afterClassClearContext() throws Exception { + ourServer.stop(); + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + @BeforeClass + public static void beforeClass() throws Exception { + ourPort = PortUtil.findFreePort(); + ourServer = new Server(ourPort); + + DummyPatientResourceProvider patientProvider = new DummyPatientResourceProvider(); + + ServletHandler proxyHandler = new ServletHandler(); + RestfulServer servlet = new RestfulServer(ourCtx); + servlet.setPagingProvider(new FifoMemoryPagingProvider(10)); + + servlet.setResourceProviders(patientProvider); + ServletHolder servletHolder = new ServletHolder(servlet); + proxyHandler.addServletWithMapping(servletHolder, "/*"); + ourServer.setHandler(proxyHandler); + ourServer.start(); + + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + builder.setConnectionManager(connectionManager); + ourClient = builder.build(); + + } + + public static class DummyPatientResourceProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Patient.class; + } + + @Search() + public IBundleProvider searchAll() { + ourLastMethod = "searchAll"; + return ourLastBundleProvider; + } + + } + +} diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/Dstu3BundleFactory.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/Dstu3BundleFactory.java index 602436df57a..e4a57134cbc 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/Dstu3BundleFactory.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/Dstu3BundleFactory.java @@ -10,7 +10,7 @@ package org.hl7.fhir.dstu3.hapi.rest.server; * 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 + * 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, @@ -45,388 +45,392 @@ import ca.uhn.fhir.util.ResourceReferenceInfo; public class Dstu3BundleFactory implements IVersionSpecificBundleFactory { - private Bundle myBundle; - private FhirContext myContext; - private String myBase; + private Bundle myBundle; + private FhirContext myContext; + private String myBase; - public Dstu3BundleFactory(FhirContext theContext) { - myContext = theContext; - } + public Dstu3BundleFactory(FhirContext theContext) { + myContext = theContext; + } - private void addResourcesForSearch(List theResult) { - List includedResources = new ArrayList(); - Set addedResourceIds = new HashSet(); + private void addResourcesForSearch(List theResult) { + List includedResources = new ArrayList(); + Set addedResourceIds = new HashSet(); - for (IBaseResource next : theResult) { - if (next.getIdElement().isEmpty() == false) { - addedResourceIds.add(next.getIdElement()); - } - } + for (IBaseResource next : theResult) { + if (next.getIdElement().isEmpty() == false) { + addedResourceIds.add(next.getIdElement()); + } + } - for (IBaseResource nextBaseRes : theResult) { - Resource next = (Resource) nextBaseRes; - Set containedIds = new HashSet(); - if (next instanceof DomainResource) { - for (Resource nextContained : ((DomainResource)next).getContained()) { - if (nextContained.getIdElement().isEmpty() == false) { - containedIds.add(nextContained.getIdElement().getValue()); - } - } - } - - List references = myContext.newTerser().getAllPopulatedChildElementsOfType(next, IBaseReference.class); - do { - List addedResourcesThisPass = new ArrayList(); + for (IBaseResource nextBaseRes : theResult) { + Resource next = (Resource) nextBaseRes; + Set containedIds = new HashSet(); + if (next instanceof DomainResource) { + for (Resource nextContained : ((DomainResource) next).getContained()) { + if (nextContained.getIdElement().isEmpty() == false) { + containedIds.add(nextContained.getIdElement().getValue()); + } + } + } - for (IBaseReference nextRef : references) { - IAnyResource nextRes = (IAnyResource) nextRef.getResource(); - if (nextRes != null) { - if (nextRes.getIdElement().hasIdPart()) { - if (containedIds.contains(nextRes.getIdElement().getValue())) { - // Don't add contained IDs as top level resources - continue; - } + List references = myContext.newTerser().getAllPopulatedChildElementsOfType(next, IBaseReference.class); + do { + List addedResourcesThisPass = new ArrayList(); - IIdType id = nextRes.getIdElement(); - if (id.hasResourceType() == false) { - String resName = myContext.getResourceDefinition(nextRes).getName(); - id = id.withResourceType(resName); - } + for (IBaseReference nextRef : references) { + IAnyResource nextRes = (IAnyResource) nextRef.getResource(); + if (nextRes != null) { + if (nextRes.getIdElement().hasIdPart()) { + if (containedIds.contains(nextRes.getIdElement().getValue())) { + // Don't add contained IDs as top level resources + continue; + } - if (!addedResourceIds.contains(id)) { - addedResourceIds.add(id); - addedResourcesThisPass.add(nextRes); - } + IIdType id = nextRes.getIdElement(); + if (id.hasResourceType() == false) { + String resName = myContext.getResourceDefinition(nextRes).getName(); + id = id.withResourceType(resName); + } - } - } - } + if (!addedResourceIds.contains(id)) { + addedResourceIds.add(id); + addedResourcesThisPass.add(nextRes); + } - // Linked resources may themselves have linked resources - references = new ArrayList(); - for (IAnyResource iResource : addedResourcesThisPass) { - List newReferences = myContext.newTerser().getAllPopulatedChildElementsOfType(iResource, IBaseReference.class); - references.addAll(newReferences); - } + } + } + } - includedResources.addAll(addedResourcesThisPass); + // Linked resources may themselves have linked resources + references = new ArrayList(); + for (IAnyResource iResource : addedResourcesThisPass) { + List newReferences = myContext.newTerser().getAllPopulatedChildElementsOfType(iResource, IBaseReference.class); + references.addAll(newReferences); + } - } while (references.isEmpty() == false); + includedResources.addAll(addedResourcesThisPass); - BundleEntryComponent entry = myBundle.addEntry().setResource(next); - if (next.getIdElement().hasBaseUrl()) { - entry.setFullUrl(next.getId()); - } + } while (references.isEmpty() == false); - String httpVerb = ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.get(next); - if (httpVerb != null) { - entry.getRequest().getMethodElement().setValueAsString(httpVerb); - entry.getRequest().getUrlElement().setValue(next.getId()); - } - } + BundleEntryComponent entry = myBundle.addEntry().setResource(next); + if (next.getIdElement().hasBaseUrl()) { + entry.setFullUrl(next.getId()); + } - /* - * Actually add the resources to the bundle - */ - for (IBaseResource next : includedResources) { - BundleEntryComponent entry = myBundle.addEntry(); - entry.setResource((Resource) next).getSearch().setMode(SearchEntryMode.INCLUDE); - if (next.getIdElement().hasBaseUrl()) { - entry.setFullUrl(next.getIdElement().getValue()); - } - } - } + String httpVerb = ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.get(next); + if (httpVerb != null) { + entry.getRequest().getMethodElement().setValueAsString(httpVerb); + entry.getRequest().getUrlElement().setValue(next.getId()); + } + } - @Override - public void addResourcesToBundle(List theResult, BundleTypeEnum theBundleType, String theServerBase, BundleInclusionRule theBundleInclusionRule, Set theIncludes) { - if (myBundle == null) { - myBundle = new Bundle(); - } + /* + * Actually add the resources to the bundle + */ + for (IBaseResource next : includedResources) { + BundleEntryComponent entry = myBundle.addEntry(); + entry.setResource((Resource) next).getSearch().setMode(SearchEntryMode.INCLUDE); + if (next.getIdElement().hasBaseUrl()) { + entry.setFullUrl(next.getIdElement().getValue()); + } + } + } - List includedResources = new ArrayList(); - Set addedResourceIds = new HashSet(); + @Override + public void addResourcesToBundle(List theResult, BundleTypeEnum theBundleType, String theServerBase, BundleInclusionRule theBundleInclusionRule, Set theIncludes) { + if (myBundle == null) { + myBundle = new Bundle(); + } - for (IBaseResource next : theResult) { - if (next.getIdElement().isEmpty() == false) { - addedResourceIds.add(next.getIdElement()); - } - } + List includedResources = new ArrayList(); + Set addedResourceIds = new HashSet(); - for (IBaseResource next : theResult) { + for (IBaseResource next : theResult) { + if (next.getIdElement().isEmpty() == false) { + addedResourceIds.add(next.getIdElement()); + } + } - Set containedIds = new HashSet(); - - if (next instanceof DomainResource) { - for (Resource nextContained : ((DomainResource)next).getContained()) { - if (isNotBlank(nextContained.getId())) { - containedIds.add(nextContained.getId()); - } - } - } + for (IBaseResource next : theResult) { - List references = myContext.newTerser().getAllResourceReferences(next); - do { - List addedResourcesThisPass = new ArrayList(); + Set containedIds = new HashSet(); - for (ResourceReferenceInfo nextRefInfo : references) { - if (!theBundleInclusionRule.shouldIncludeReferencedResource(nextRefInfo, theIncludes)) { - continue; - } + if (next instanceof DomainResource) { + for (Resource nextContained : ((DomainResource) next).getContained()) { + if (isNotBlank(nextContained.getId())) { + containedIds.add(nextContained.getId()); + } + } + } - IAnyResource nextRes = (IAnyResource) nextRefInfo.getResourceReference().getResource(); - if (nextRes != null) { - if (nextRes.getIdElement().hasIdPart()) { - if (containedIds.contains(nextRes.getIdElement().getValue())) { - // Don't add contained IDs as top level resources - continue; - } + List references = myContext.newTerser().getAllResourceReferences(next); + do { + List addedResourcesThisPass = new ArrayList(); - IIdType id = nextRes.getIdElement(); - if (id.hasResourceType() == false) { - String resName = myContext.getResourceDefinition(nextRes).getName(); - id = id.withResourceType(resName); - } + for (ResourceReferenceInfo nextRefInfo : references) { + if (!theBundleInclusionRule.shouldIncludeReferencedResource(nextRefInfo, theIncludes)) { + continue; + } - if (!addedResourceIds.contains(id)) { - addedResourceIds.add(id); - addedResourcesThisPass.add(nextRes); - } + IAnyResource nextRes = (IAnyResource) nextRefInfo.getResourceReference().getResource(); + if (nextRes != null) { + if (nextRes.getIdElement().hasIdPart()) { + if (containedIds.contains(nextRes.getIdElement().getValue())) { + // Don't add contained IDs as top level resources + continue; + } - } - } - } + IIdType id = nextRes.getIdElement(); + if (id.hasResourceType() == false) { + String resName = myContext.getResourceDefinition(nextRes).getName(); + id = id.withResourceType(resName); + } - includedResources.addAll(addedResourcesThisPass); + if (!addedResourceIds.contains(id)) { + addedResourceIds.add(id); + addedResourcesThisPass.add(nextRes); + } - // Linked resources may themselves have linked resources - references = new ArrayList(); - for (IAnyResource iResource : addedResourcesThisPass) { - List newReferences = myContext.newTerser().getAllResourceReferences(iResource); - references.addAll(newReferences); - } - } while (references.isEmpty() == false); + } + } + } - BundleEntryComponent entry = myBundle.addEntry().setResource((Resource) next); - Resource nextAsResource = (Resource)next; - IIdType id = populateBundleEntryFullUrl(next, entry); - String httpVerb = ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.get(nextAsResource); - if (httpVerb != null) { - entry.getRequest().getMethodElement().setValueAsString(httpVerb); - if (id != null) { - entry.getRequest().setUrl(id.getValue()); - } - } - - String searchMode = ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(nextAsResource); - if (searchMode != null) { - entry.getSearch().getModeElement().setValueAsString(searchMode); - } - } + includedResources.addAll(addedResourcesThisPass); - /* - * Actually add the resources to the bundle - */ - for (IAnyResource next : includedResources) { - BundleEntryComponent entry = myBundle.addEntry(); - entry.setResource((Resource) next).getSearch().setMode(SearchEntryMode.INCLUDE); - populateBundleEntryFullUrl(next, entry); - } + // Linked resources may themselves have linked resources + references = new ArrayList(); + for (IAnyResource iResource : addedResourcesThisPass) { + List newReferences = myContext.newTerser().getAllResourceReferences(iResource); + references.addAll(newReferences); + } + } while (references.isEmpty() == false); - } + BundleEntryComponent entry = myBundle.addEntry().setResource((Resource) next); + Resource nextAsResource = (Resource) next; + IIdType id = populateBundleEntryFullUrl(next, entry); + String httpVerb = ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.get(nextAsResource); + if (httpVerb != null) { + entry.getRequest().getMethodElement().setValueAsString(httpVerb); + if (id != null) { + entry.getRequest().setUrl(id.getValue()); + } + } - private IIdType populateBundleEntryFullUrl(IBaseResource next, BundleEntryComponent entry) { - IIdType idElement = null; - if (next.getIdElement().hasBaseUrl()) { - idElement = next.getIdElement(); - entry.setFullUrl(idElement.toVersionless().getValue()); - } else { - if (isNotBlank(myBase) && next.getIdElement().hasIdPart()) { - idElement = next.getIdElement(); - idElement = idElement.withServerBase(myBase, myContext.getResourceDefinition(next).getName()); - entry.setFullUrl(idElement.toVersionless().getValue()); - } - } - return idElement; - } + String searchMode = ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(nextAsResource); + if (searchMode != null) { + entry.getSearch().getModeElement().setValueAsString(searchMode); + } + } - @Override - public void addRootPropertiesToBundle(String theAuthor, String theServerBase, String theCompleteUrl, Integer theTotalResults, BundleTypeEnum theBundleType, IPrimitiveType theLastUpdated) { + /* + * Actually add the resources to the bundle + */ + for (IAnyResource next : includedResources) { + BundleEntryComponent entry = myBundle.addEntry(); + entry.setResource((Resource) next).getSearch().setMode(SearchEntryMode.INCLUDE); + populateBundleEntryFullUrl(next, entry); + } - myBase = theServerBase; - - if (myBundle.getIdElement().isEmpty()) { - myBundle.setId(UUID.randomUUID().toString()); - } + } - if (myBundle.getMeta().getLastUpdated() == null && theLastUpdated != null) { - myBundle.getMeta().getLastUpdatedElement().setValueAsString(theLastUpdated.getValueAsString()); - } + private IIdType populateBundleEntryFullUrl(IBaseResource next, BundleEntryComponent entry) { + IIdType idElement = null; + if (next.getIdElement().hasBaseUrl()) { + idElement = next.getIdElement(); + entry.setFullUrl(idElement.toVersionless().getValue()); + } else { + if (isNotBlank(myBase) && next.getIdElement().hasIdPart()) { + idElement = next.getIdElement(); + idElement = idElement.withServerBase(myBase, myContext.getResourceDefinition(next).getName()); + entry.setFullUrl(idElement.toVersionless().getValue()); + } + } + return idElement; + } - if (!hasLink(Constants.LINK_SELF, myBundle) && isNotBlank(theCompleteUrl)) { - myBundle.addLink().setRelation("self").setUrl(theCompleteUrl); - } + @Override + public void addRootPropertiesToBundle(String theAuthor, String theServerBase, String theCompleteUrl, Integer theTotalResults, BundleTypeEnum theBundleType, IPrimitiveType theLastUpdated) { - if (myBundle.getTypeElement().isEmpty() && theBundleType != null) { - myBundle.getTypeElement().setValueAsString(theBundleType.getCode()); - } + myBase = theServerBase; - if (myBundle.getTotalElement().isEmpty() && theTotalResults != null) { - myBundle.getTotalElement().setValue(theTotalResults); - } - } + if (myBundle.getIdElement().isEmpty()) { + myBundle.setId(UUID.randomUUID().toString()); + } - @Override - public ca.uhn.fhir.model.api.Bundle getDstu1Bundle() { - return null; - } + if (myBundle.getMeta().getLastUpdated() == null && theLastUpdated != null) { + myBundle.getMeta().getLastUpdatedElement().setValueAsString(theLastUpdated.getValueAsString()); + } - @Override - public IBaseResource getResourceBundle() { - return myBundle; - } + if (!hasLink(Constants.LINK_SELF, myBundle) && isNotBlank(theCompleteUrl)) { + myBundle.addLink().setRelation("self").setUrl(theCompleteUrl); + } - private boolean hasLink(String theLinkType, Bundle theBundle) { - for (BundleLinkComponent next : theBundle.getLink()) { - if (theLinkType.equals(next.getRelation())) { - return true; - } - } - return false; - } + if (myBundle.getTypeElement().isEmpty() && theBundleType != null) { + myBundle.getTypeElement().setValueAsString(theBundleType.getCode()); + } - @Override - public void initializeBundleFromBundleProvider(IRestfulServer theServer, IBundleProvider theResult, EncodingEnum theResponseEncoding, String theServerBase, String theCompleteUrl, - boolean thePrettyPrint, int theOffset, Integer theLimit, String theSearchId, BundleTypeEnum theBundleType, Set theIncludes) { - myBase = theServerBase; - - int numToReturn; - String searchId = null; - List resourceList; - if (theServer.getPagingProvider() == null) { - numToReturn = theResult.size(); - if (numToReturn > 0) { - resourceList = theResult.getResources(0, numToReturn); - } else { - resourceList = Collections.emptyList(); - } - RestfulServerUtils.validateResourceListNotNull(resourceList); + if (myBundle.getTotalElement().isEmpty() && theTotalResults != null) { + myBundle.getTotalElement().setValue(theTotalResults); + } + } - } else { - IPagingProvider pagingProvider = theServer.getPagingProvider(); - if (theLimit == null) { - numToReturn = pagingProvider.getDefaultPageSize(); - } else { - numToReturn = Math.min(pagingProvider.getMaximumPageSize(), theLimit); - } + @Override + public ca.uhn.fhir.model.api.Bundle getDstu1Bundle() { + return null; + } - numToReturn = Math.min(numToReturn, theResult.size() - theOffset); - if (numToReturn > 0) { - resourceList = theResult.getResources(theOffset, numToReturn + theOffset); - } else { - resourceList = Collections.emptyList(); - } - RestfulServerUtils.validateResourceListNotNull(resourceList); + @Override + public IBaseResource getResourceBundle() { + return myBundle; + } - if (theSearchId != null) { - searchId = theSearchId; - } else { - if (theResult.size() > numToReturn) { - searchId = pagingProvider.storeResultList(theResult); - Validate.notNull(searchId, "Paging provider returned null searchId"); - } - } - } + private boolean hasLink(String theLinkType, Bundle theBundle) { + for (BundleLinkComponent next : theBundle.getLink()) { + if (theLinkType.equals(next.getRelation())) { + return true; + } + } + return false; + } - for (IBaseResource next : resourceList) { - if (next.getIdElement() == null || next.getIdElement().isEmpty()) { - if (!(next instanceof BaseOperationOutcome)) { - throw new InternalErrorException("Server method returned resource of type[" + next.getClass().getSimpleName() + "] with no ID specified (IResource#setId(IdDt) must be called)"); - } - } - } + @Override + public void initializeBundleFromBundleProvider(IRestfulServer theServer, IBundleProvider theResult, EncodingEnum theResponseEncoding, String theServerBase, String theCompleteUrl, + boolean thePrettyPrint, int theOffset, Integer theLimit, String theSearchId, BundleTypeEnum theBundleType, Set theIncludes) { + myBase = theServerBase; - addResourcesToBundle(new ArrayList(resourceList), theBundleType, theServerBase, theServer.getBundleInclusionRule(), theIncludes); - addRootPropertiesToBundle(null, theServerBase, theCompleteUrl, theResult.size(), theBundleType, theResult.getPublished()); + int numToReturn; + String searchId = null; + List resourceList; + Integer numTotalResults = theResult.size(); + if (theServer.getPagingProvider() == null) { + numToReturn = numTotalResults; + if (numToReturn > 0) { + resourceList = theResult.getResources(0, numToReturn); + } else { + resourceList = Collections.emptyList(); + } + RestfulServerUtils.validateResourceListNotNull(resourceList); - if (theServer.getPagingProvider() != null) { - int limit; - limit = theLimit != null ? theLimit : theServer.getPagingProvider().getDefaultPageSize(); - limit = Math.min(limit, theServer.getPagingProvider().getMaximumPageSize()); + } else { + IPagingProvider pagingProvider = theServer.getPagingProvider(); + if (theLimit == null || theLimit.equals(Integer.valueOf(0))) { + numToReturn = pagingProvider.getDefaultPageSize(); + } else { + numToReturn = Math.min(pagingProvider.getMaximumPageSize(), theLimit); + } - if (searchId != null) { - if (theOffset + numToReturn < theResult.size()) { - myBundle.addLink().setRelation(Constants.LINK_NEXT) - .setUrl(RestfulServerUtils.createPagingLink(theIncludes, theServerBase, searchId, theOffset + numToReturn, numToReturn, theResponseEncoding, thePrettyPrint, theBundleType)); - } - if (theOffset > 0) { - int start = Math.max(0, theOffset - limit); - myBundle.addLink().setRelation(Constants.LINK_PREVIOUS) - .setUrl(RestfulServerUtils.createPagingLink(theIncludes, theServerBase, searchId, start, limit, theResponseEncoding, thePrettyPrint, theBundleType)); - } - } - } - } + if (numTotalResults != null) { + numToReturn = Math.min(numToReturn, numTotalResults - theOffset); + } - @Override - public void initializeBundleFromResourceList(String theAuthor, List theResources, String theServerBase, String theCompleteUrl, int theTotalResults, - BundleTypeEnum theBundleType) { - myBundle = new Bundle(); + if (numToReturn > 0) { + resourceList = theResult.getResources(theOffset, numToReturn + theOffset); + } else { + resourceList = Collections.emptyList(); + } + RestfulServerUtils.validateResourceListNotNull(resourceList); - myBundle.setId(UUID.randomUUID().toString()); + if (theSearchId != null) { + searchId = theSearchId; + } else { + if (numTotalResults == null || numTotalResults > numToReturn) { + searchId = pagingProvider.storeResultList(theResult); + Validate.notNull(searchId, "Paging provider returned null searchId"); + } + } + } - myBundle.getMeta().setLastUpdated(new Date()); + for (IBaseResource next : resourceList) { + if (next.getIdElement() == null || next.getIdElement().isEmpty()) { + if (!(next instanceof BaseOperationOutcome)) { + throw new InternalErrorException("Server method returned resource of type[" + next.getClass().getSimpleName() + "] with no ID specified (IResource#setId(IdDt) must be called)"); + } + } + } - myBundle.addLink().setRelation(Constants.LINK_FHIR_BASE).setUrl(theServerBase); - myBundle.addLink().setRelation(Constants.LINK_SELF).setUrl(theCompleteUrl); - myBundle.getTypeElement().setValueAsString(theBundleType.getCode()); + addResourcesToBundle(new ArrayList(resourceList), theBundleType, theServerBase, theServer.getBundleInclusionRule(), theIncludes); + addRootPropertiesToBundle(null, theServerBase, theCompleteUrl, theResult.size(), theBundleType, theResult.getPublished()); - if (theBundleType.equals(BundleTypeEnum.TRANSACTION)) { - for (IBaseResource nextBaseRes : theResources) { - Resource next = (Resource) nextBaseRes; - BundleEntryComponent nextEntry = myBundle.addEntry(); + if (theServer.getPagingProvider() != null) { + int limit; + limit = theLimit != null ? theLimit : theServer.getPagingProvider().getDefaultPageSize(); + limit = Math.min(limit, theServer.getPagingProvider().getMaximumPageSize()); - nextEntry.setResource(next); - if (next.getIdElement().isEmpty()) { - nextEntry.getRequest().setMethod(HTTPVerb.POST); - } else { - nextEntry.getRequest().setMethod(HTTPVerb.PUT); - if (next.getIdElement().isAbsolute()) { - nextEntry.getRequest().setUrl(next.getId()); - } else { - String resourceType = myContext.getResourceDefinition(next).getName(); - nextEntry.getRequest().setUrl(new IdType(theServerBase, resourceType, next.getIdElement().getIdPart(), next.getIdElement().getVersionIdPart()).getValue()); - } - } - } - } else { - addResourcesForSearch(theResources); - } + if (searchId != null) { + if (numTotalResults == null || theOffset + numToReturn < numTotalResults) { + myBundle.addLink().setRelation(Constants.LINK_NEXT) + .setUrl(RestfulServerUtils.createPagingLink(theIncludes, theServerBase, searchId, theOffset + numToReturn, numToReturn, theResponseEncoding, thePrettyPrint, theBundleType)); + } + if (theOffset > 0) { + int start = Math.max(0, theOffset - limit); + myBundle.addLink().setRelation(Constants.LINK_PREVIOUS) + .setUrl(RestfulServerUtils.createPagingLink(theIncludes, theServerBase, searchId, start, limit, theResponseEncoding, thePrettyPrint, theBundleType)); + } + } + } + } - myBundle.getTotalElement().setValue(theTotalResults); - } + @Override + public void initializeBundleFromResourceList(String theAuthor, List theResources, String theServerBase, String theCompleteUrl, int theTotalResults, + BundleTypeEnum theBundleType) { + myBundle = new Bundle(); - @Override - public void initializeWithBundleResource(IBaseResource theBundle) { - myBundle = (Bundle) theBundle; - } + myBundle.setId(UUID.randomUUID().toString()); - @Override - public List toListOfResources() { - ArrayList retVal = new ArrayList(); - for (BundleEntryComponent next : myBundle.getEntry()) { - if (next.getResource() != null) { - retVal.add(next.getResource()); - } else if (next.getResponse().getLocationElement().isEmpty() == false) { - IdType id = new IdType(next.getResponse().getLocation()); - String resourceType = id.getResourceType(); - if (isNotBlank(resourceType)) { - IAnyResource res = (IAnyResource) myContext.getResourceDefinition(resourceType).newInstance(); - res.setId(id); - retVal.add(res); - } - } - } - return retVal; - } + myBundle.getMeta().setLastUpdated(new Date()); + + myBundle.addLink().setRelation(Constants.LINK_FHIR_BASE).setUrl(theServerBase); + myBundle.addLink().setRelation(Constants.LINK_SELF).setUrl(theCompleteUrl); + myBundle.getTypeElement().setValueAsString(theBundleType.getCode()); + + if (theBundleType.equals(BundleTypeEnum.TRANSACTION)) { + for (IBaseResource nextBaseRes : theResources) { + Resource next = (Resource) nextBaseRes; + BundleEntryComponent nextEntry = myBundle.addEntry(); + + nextEntry.setResource(next); + if (next.getIdElement().isEmpty()) { + nextEntry.getRequest().setMethod(HTTPVerb.POST); + } else { + nextEntry.getRequest().setMethod(HTTPVerb.PUT); + if (next.getIdElement().isAbsolute()) { + nextEntry.getRequest().setUrl(next.getId()); + } else { + String resourceType = myContext.getResourceDefinition(next).getName(); + nextEntry.getRequest().setUrl(new IdType(theServerBase, resourceType, next.getIdElement().getIdPart(), next.getIdElement().getVersionIdPart()).getValue()); + } + } + } + } else { + addResourcesForSearch(theResources); + } + + myBundle.getTotalElement().setValue(theTotalResults); + } + + @Override + public void initializeWithBundleResource(IBaseResource theBundle) { + myBundle = (Bundle) theBundle; + } + + @Override + public List toListOfResources() { + ArrayList retVal = new ArrayList(); + for (BundleEntryComponent next : myBundle.getEntry()) { + if (next.getResource() != null) { + retVal.add(next.getResource()); + } else if (next.getResponse().getLocationElement().isEmpty() == false) { + IdType id = new IdType(next.getResponse().getLocation()); + String resourceType = id.getResourceType(); + if (isNotBlank(resourceType)) { + IAnyResource res = (IAnyResource) myContext.getResourceDefinition(resourceType).newInstance(); + res.setId(id); + retVal.add(res); + } + } + } + return retVal; + } } diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/SearchBundleProviderWithNoSizeDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/SearchBundleProviderWithNoSizeDstu3Test.java index 29f5f1ec001..a749dd82add 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/SearchBundleProviderWithNoSizeDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/SearchBundleProviderWithNoSizeDstu3Test.java @@ -1,8 +1,7 @@ package ca.uhn.fhir.rest.server; -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -19,6 +18,8 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.hl7.fhir.dstu3.model.Bundle; +import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.dstu3.model.Bundle.BundleLinkComponent; import org.hl7.fhir.dstu3.model.HumanName; import org.hl7.fhir.dstu3.model.OperationOutcome; import org.hl7.fhir.dstu3.model.Patient; @@ -27,6 +28,10 @@ import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import com.google.common.collect.Lists; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.annotation.RequiredParam; @@ -58,16 +63,78 @@ public class SearchBundleProviderWithNoSizeDstu3Test { Bundle respBundle; ourLastBundleProvider = mock(IBundleProvider.class); - when(ourLastBundleProvider.size()).thenReturn(-1); + when(ourLastBundleProvider.size()).thenReturn(null); + when(ourLastBundleProvider.getResources(any(int.class), any(int.class))).then(new Answer>() { + @Override + public List answer(InvocationOnMock theInvocation) throws Throwable { + int from =(Integer)theInvocation.getArguments()[0]; + int to =(Integer)theInvocation.getArguments()[1]; + ArrayList retVal = Lists.newArrayList(); + for (int i = from; i < to; i++) { + Patient p = new Patient(); + p.setId(Integer.toString(i)); + retVal.add(p); + } + return retVal; + }}); + + HttpGet httpGet; + CloseableHttpResponse status = null; + BundleLinkComponent linkNext; - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_format=json"); - CloseableHttpResponse status = ourClient.execute(httpGet); try { + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_format=json"); + status = ourClient.execute(httpGet); String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info(responseContent); assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals("searchAll", ourLastMethod); respBundle = ourCtx.newJsonParser().parseResource(Bundle.class, responseContent); + + assertEquals(10, respBundle.getEntry().size()); + assertEquals("Patient/0", respBundle.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless().getValue()); + linkNext = respBundle.getLink("next"); + assertNotNull(linkNext); + + } finally { + IOUtils.closeQuietly(status.getEntity().getContent()); + } + + + when(ourLastBundleProvider.size()).thenReturn(25); + + try { + httpGet = new HttpGet(linkNext.getUrl()); + status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(responseContent); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals("searchAll", ourLastMethod); + respBundle = ourCtx.newJsonParser().parseResource(Bundle.class, responseContent); + + assertEquals(10, respBundle.getEntry().size()); + assertEquals("Patient/10", respBundle.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless().getValue()); + linkNext = respBundle.getLink("next"); + assertNotNull(linkNext); + + } finally { + IOUtils.closeQuietly(status.getEntity().getContent()); + } + + try { + httpGet = new HttpGet(linkNext.getUrl()); + status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(responseContent); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals("searchAll", ourLastMethod); + respBundle = ourCtx.newJsonParser().parseResource(Bundle.class, responseContent); + + assertEquals(5, respBundle.getEntry().size()); + assertEquals("Patient/20", respBundle.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless().getValue()); + linkNext = respBundle.getLink("next"); + assertNull(linkNext); + } finally { IOUtils.closeQuietly(status.getEntity().getContent()); }