adding a test

This commit is contained in:
leif stawnyczy 2023-08-16 13:21:01 -04:00
parent 667421db77
commit 2c0f157ce9
13 changed files with 358 additions and 53 deletions

View File

@ -24,6 +24,7 @@ import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.method.ResponsePage;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.InstantType; import org.hl7.fhir.r4.model.InstantType;
import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Patient;
@ -63,7 +64,7 @@ public class PagingPatientProvider implements IResourceProvider {
@Nonnull @Nonnull
@Override @Override
public List<IBaseResource> getResources(int theFromIndex, int theToIndex) { public List<IBaseResource> getResources(int theFromIndex, int theToIndex, @Nonnull ResponsePage.ResponsePageBuilder theResponsePageBuilder) {
int end = Math.max(theToIndex, matchingResourceIds.size() - 1); int end = Math.max(theToIndex, matchingResourceIds.size() - 1);
List<Long> idsToReturn = matchingResourceIds.subList(theFromIndex, end); List<Long> idsToReturn = matchingResourceIds.subList(theFromIndex, end);
return loadResourcesByIds(idsToReturn); return loadResourcesByIds(idsToReturn);

View File

@ -55,23 +55,25 @@ import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SimplePreResourceAccessDetails; import ca.uhn.fhir.rest.api.server.SimplePreResourceAccessDetails;
import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails; import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails;
import ca.uhn.fhir.rest.server.interceptor.ServerInterceptorUtil; import ca.uhn.fhir.rest.server.interceptor.ServerInterceptorUtil;
import ca.uhn.fhir.rest.server.method.ResponsePage;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.Nonnull;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import javax.annotation.Nonnull;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
public class PersistedJpaBundleProvider implements IBundleProvider { public class PersistedJpaBundleProvider implements IBundleProvider {
@ -128,6 +130,7 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
private String myUuid; private String myUuid;
private SearchCacheStatusEnum myCacheStatus; private SearchCacheStatusEnum myCacheStatus;
private RequestPartitionId myRequestPartitionId; private RequestPartitionId myRequestPartitionId;
/** /**
* Constructor * Constructor
*/ */
@ -150,6 +153,13 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
myRequestPartitionHelperSvc = theRequestPartitionHelperSvc; myRequestPartitionHelperSvc = theRequestPartitionHelperSvc;
} }
@Override
public ResponsePage.ResponsePageBuilder getResponsePageBuilder() {
ResponsePage.ResponsePageBuilder builder = new ResponsePage.ResponsePageBuilder();
return builder;
}
protected Search getSearchEntity() { protected Search getSearchEntity() {
return mySearchEntity; return mySearchEntity;
} }
@ -163,7 +173,9 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
/** /**
* Perform a history search * Perform a history search
*/ */
private List<IBaseResource> doHistoryInTransaction(Integer theOffset, int theFromIndex, int theToIndex) { private List<IBaseResource> doHistoryInTransaction(
Integer theOffset, int theFromIndex, int theToIndex
) {
HistoryBuilder historyBuilder = myHistoryBuilderFactory.newHistoryBuilder( HistoryBuilder historyBuilder = myHistoryBuilderFactory.newHistoryBuilder(
mySearchEntity.getResourceType(), mySearchEntity.getResourceType(),
@ -180,7 +192,6 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
BaseHasResource resource; BaseHasResource resource;
resource = next; resource = next;
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(next.getResourceType());
retVal.add(myJpaStorageResourceParser.toResource(resource, true)); retVal.add(myJpaStorageResourceParser.toResource(resource, true));
} }
@ -238,7 +249,9 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
myRequestPartitionId = theRequestPartitionId; myRequestPartitionId = theRequestPartitionId;
} }
protected List<IBaseResource> doSearchOrEverything(final int theFromIndex, final int theToIndex) { protected List<IBaseResource> doSearchOrEverything(
final int theFromIndex, final int theToIndex, ResponsePage.ResponsePageBuilder theResponsePageBuilder
) {
if (mySearchEntity.getTotalCount() != null && mySearchEntity.getNumFound() <= 0) { if (mySearchEntity.getTotalCount() != null && mySearchEntity.getNumFound() <= 0) {
// No resources to fetch (e.g. we did a _summary=count search) // No resources to fetch (e.g. we did a _summary=count search)
return Collections.emptyList(); return Collections.emptyList();
@ -253,12 +266,14 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
RequestPartitionId requestPartitionId = getRequestPartitionId(); RequestPartitionId requestPartitionId = getRequestPartitionId();
final List<JpaPid> pidsSubList = final List<JpaPid> pidsSubList =
mySearchCoordinatorSvc.getResources(myUuid, theFromIndex, theToIndex, myRequest, requestPartitionId); mySearchCoordinatorSvc.getResources(myUuid, theFromIndex, theToIndex, myRequest, requestPartitionId);
return myTxService List<IBaseResource> resources = myTxService
.withRequest(myRequest) .withRequest(myRequest)
.withRequestPartitionId(requestPartitionId) .withRequestPartitionId(requestPartitionId)
.execute(() -> { .execute(() -> {
return toResourceList(sb, pidsSubList); return toResourceList(sb, pidsSubList, theResponsePageBuilder);
}); });
return resources;
} }
/** /**
@ -349,9 +364,16 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
return new InstantDt(mySearchEntity.getCreated()); return new InstantDt(mySearchEntity.getCreated());
} }
@Nonnull @NotNull
@Override @Override
public List<IBaseResource> getResources(final int theFromIndex, final int theToIndex) { public List<IBaseResource> getResources(int theFromIndex, int theToIndex) {
return getResources(theFromIndex, theToIndex, getResponsePageBuilder());
}
@Override
public List<IBaseResource> getResources(
int theFromIndex, int theToIndex, @Nonnull ResponsePage.ResponsePageBuilder theResponsePageBuilder
) {
boolean entityLoaded = ensureSearchEntityLoaded(); boolean entityLoaded = ensureSearchEntityLoaded();
assert entityLoaded; assert entityLoaded;
assert mySearchEntity != null; assert mySearchEntity != null;
@ -366,7 +388,7 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
case SEARCH: case SEARCH:
case EVERYTHING: case EVERYTHING:
default: default:
List<IBaseResource> retVal = doSearchOrEverything(theFromIndex, theToIndex); List<IBaseResource> retVal = doSearchOrEverything(theFromIndex, theToIndex, theResponsePageBuilder);
/* /*
* If we got fewer resources back than we asked for, it's possible that the search * If we got fewer resources back than we asked for, it's possible that the search
* completed. If that's the case, the cached version of the search entity is probably * completed. If that's the case, the cached version of the search entity is probably
@ -443,8 +465,11 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
// Note: Leave as protected, HSPC depends on this // Note: Leave as protected, HSPC depends on this
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
protected List<IBaseResource> toResourceList(ISearchBuilder theSearchBuilder, List<JpaPid> thePids) { protected List<IBaseResource> toResourceList(
ISearchBuilder theSearchBuilder,
List<JpaPid> thePids,
ResponsePage.ResponsePageBuilder theResponsePageBuilder
) {
List<JpaPid> includedPidList = new ArrayList<>(); List<JpaPid> includedPidList = new ArrayList<>();
if (mySearchEntity.getSearchType() == SearchTypeEnum.SEARCH) { if (mySearchEntity.getSearchType() == SearchTypeEnum.SEARCH) {
Integer maxIncludes = myStorageSettings.getMaximumIncludesToLoadPerPage(); Integer maxIncludes = myStorageSettings.getMaximumIncludesToLoadPerPage();
@ -522,6 +547,8 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
theSearchBuilder.loadResourcesByPid(thePids, includedPidList, resources, false, myRequest); theSearchBuilder.loadResourcesByPid(thePids, includedPidList, resources, false, myRequest);
resources = ServerInterceptorUtil.fireStoragePreshowResource(resources, myRequest, myInterceptorBroadcaster); resources = ServerInterceptorUtil.fireStoragePreshowResource(resources, myRequest, myInterceptorBroadcaster);
theResponsePageBuilder.setResources(resources);
theResponsePageBuilder.setIncludedResourceCount(includedPidList.size());
return resources; return resources;
} }

View File

@ -30,23 +30,26 @@ import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.method.ResponsePage;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Nonnull;
public class PersistedJpaSearchFirstPageBundleProvider extends PersistedJpaBundleProvider { public class PersistedJpaSearchFirstPageBundleProvider extends PersistedJpaBundleProvider {
private static final Logger ourLog = LoggerFactory.getLogger(PersistedJpaSearchFirstPageBundleProvider.class); private static final Logger ourLog = LoggerFactory.getLogger(PersistedJpaSearchFirstPageBundleProvider.class);
private final SearchTask mySearchTask; private final SearchTask mySearchTask;
@SuppressWarnings("rawtypes")
private final ISearchBuilder mySearchBuilder; private final ISearchBuilder mySearchBuilder;
/** /**
* Constructor * Constructor
*/ */
@SuppressWarnings("rawtypes")
public PersistedJpaSearchFirstPageBundleProvider( public PersistedJpaSearchFirstPageBundleProvider(
Search theSearch, Search theSearch,
SearchTask theSearchTask, SearchTask theSearchTask,
@ -65,7 +68,7 @@ public class PersistedJpaSearchFirstPageBundleProvider extends PersistedJpaBundl
@Nonnull @Nonnull
@Override @Override
public List<IBaseResource> getResources(int theFromIndex, int theToIndex) { public List<IBaseResource> getResources(int theFromIndex, int theToIndex, @Nonnull ResponsePage.ResponsePageBuilder thePageBuilder) {
ensureSearchEntityLoaded(); ensureSearchEntityLoaded();
QueryParameterUtils.verifySearchHasntFailedOrThrowInternalErrorException(getSearchEntity()); QueryParameterUtils.verifySearchHasntFailedOrThrowInternalErrorException(getSearchEntity());
@ -80,7 +83,7 @@ public class PersistedJpaSearchFirstPageBundleProvider extends PersistedJpaBundl
List<IBaseResource> retVal = myTxService List<IBaseResource> retVal = myTxService
.withRequest(myRequest) .withRequest(myRequest)
.withRequestPartitionId(requestPartitionId) .withRequestPartitionId(requestPartitionId)
.execute(() -> toResourceList(mySearchBuilder, pids)); .execute(() -> toResourceList(mySearchBuilder, pids, thePageBuilder));
long totalCountWanted = theToIndex - theFromIndex; long totalCountWanted = theToIndex - theFromIndex;
long totalCountMatch = (int) retVal.stream().filter(t -> !isInclude(t)).count(); long totalCountMatch = (int) retVal.stream().filter(t -> !isInclude(t)).count();
@ -101,7 +104,7 @@ public class PersistedJpaSearchFirstPageBundleProvider extends PersistedJpaBundl
long remainingWanted = totalCountWanted - totalCountMatch; long remainingWanted = totalCountWanted - totalCountMatch;
long fromIndex = theToIndex - remainingWanted; long fromIndex = theToIndex - remainingWanted;
List<IBaseResource> remaining = super.getResources((int) fromIndex, theToIndex); List<IBaseResource> remaining = super.getResources((int) fromIndex, theToIndex, thePageBuilder);
remaining.forEach(t -> { remaining.forEach(t -> {
if (!existingIds.contains(t.getIdElement().getValue())) { if (!existingIds.contains(t.getIdElement().getValue())) {
retVal.add(t); retVal.add(t);

View File

@ -146,6 +146,7 @@ import org.hl7.fhir.r4.model.StructureDefinition;
import org.hl7.fhir.r4.model.Subscription; import org.hl7.fhir.r4.model.Subscription;
import org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType; import org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType;
import org.hl7.fhir.r4.model.Subscription.SubscriptionStatus; import org.hl7.fhir.r4.model.Subscription.SubscriptionStatus;
import org.hl7.fhir.r4.model.Task;
import org.hl7.fhir.r4.model.UriType; import org.hl7.fhir.r4.model.UriType;
import org.hl7.fhir.r4.model.ValueSet; import org.hl7.fhir.r4.model.ValueSet;
import org.hl7.fhir.utilities.xhtml.NodeType; import org.hl7.fhir.utilities.xhtml.NodeType;
@ -2654,6 +2655,92 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
} }
private void printResources(Bundle theBundle) {
IParser parser = myFhirContext.newJsonParser();
for (BundleEntryComponent entry : theBundle.getEntry()) {
ourLog.info(entry.getResource().getId());
// ourLog.info(
// parser.encodeResourceToString(entry.getResource())
// );
}
}
// TODO - we should not be including the
// _include count in the _count
// so current behaviour is correct
// but we should also not be including a next
// list if there are no more resources
@Test
public void testPagingWithIncludesReturnsConsistentValues() {
// setup
int total = 19;
int orgs = 10;
// create resources
{
Coding tagCode = new Coding();
tagCode.setCode("test");
tagCode.setSystem("http://example.com");
int orgCount = orgs;
for (int i = 0; i < total; i++) {
Task t = new Task();
t.getMeta()
.addTag(tagCode);
t.setStatus(Task.TaskStatus.REQUESTED);
if (orgCount > 0) {
Organization org = new Organization();
org.setName("ORG");
IIdType orgId = myOrganizationDao.create(org).getId().toUnqualifiedVersionless();
orgCount--;
t.getOwner().setReference(orgId.getValue());
}
myTaskDao.create(t);
}
}
int requestedAmount = 10;
Bundle bundle = myClient
.search()
.byUrl("Task?_count=10&_tag=test&status=requested&_include=Task%3Aowner&_sort=status")
.returnBundle(Bundle.class)
.execute();
int count = bundle.getEntry().size();
assertTrue(bundle.getEntry().size() > 0);
System.out.println("Received " + bundle.getEntry().size());
// assertEquals(requestedAmount, count);
printResources(bundle);
String nextUrl = null;
do {
Bundle.BundleLinkComponent nextLink = bundle.getLink("next");
if (nextLink != null) {
nextUrl = nextLink.getUrl();
// make sure we're always requesting 10
assertTrue(nextUrl.contains(String.format("_count=%d", requestedAmount)));
// get next batch
bundle = myClient.fetchResourceFromUrl(Bundle.class, nextUrl);
int received = bundle.getEntry().size();
// verify it's the same amount as we requested
System.out.println("Recieved " + received);
printResources(bundle);
assertTrue(bundle.getEntry().size() > 0);
// assertEquals(requestedAmount, received);
count += received;
} else {
nextUrl = null;
}
} while (nextUrl != null);
// verify
assertEquals(total + orgs, count);
}
/** /**
* See #793 * See #793
*/ */

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.rest.api.server;
import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.rest.server.method.ResponsePage;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.instance.model.api.IPrimitiveType;
@ -34,6 +35,14 @@ import javax.annotation.Nullable;
public interface IBundleProvider { public interface IBundleProvider {
/**
* Returns a ResponsePageBuilder for constructing
* pages that return results.
*/
default ResponsePage.ResponsePageBuilder getResponsePageBuilder() {
return new ResponsePage.ResponsePageBuilder();
}
/** /**
* If this method is implemented, provides an ID for the current * If this method is implemented, provides an ID for the current
* page of results. This ID should be unique (at least within * page of results. This ID should be unique (at least within
@ -113,6 +122,32 @@ public interface IBundleProvider {
*/ */
IPrimitiveType<Date> getPublished(); IPrimitiveType<Date> getPublished();
/**
* Deprecated: Use the getResources(int from, int to, ResourcePageBuilder builder) instead.
* <p>
* Load the given collection of resources by index, plus any additional resources per the
* server's processing rules (e.g. _include'd resources, OperationOutcome, etc.). For example,
* if the method is invoked with index 0,10 the method might return 10 search results, plus an
* additional 20 resources which matched a client's _include specification.
* </p>
* <p>
* Note that if this bundle provider was loaded using a
* page ID (i.e. via {@link ca.uhn.fhir.rest.server.IPagingProvider#retrieveResultList(RequestDetails, String, String)}
* because {@link #getNextPageId()} provided a value on the
* previous page, then the indexes should be ignored and the
* whole page returned.
* </p>
*
* @param theFromIndex The low index (inclusive) to return
* @param theToIndex The high index (exclusive) to return
* @return A list of resources. The size of this list must be at least <code>theToIndex - theFromIndex</code>.
*/
@Nonnull
@Deprecated(forRemoval = true, since = "7.0.0")
default List<IBaseResource> getResources(int theFromIndex, int theToIndex) {
return getResources(theFromIndex, theToIndex, getResponsePageBuilder());
}
/** /**
* Load the given collection of resources by index, plus any additional resources per the * Load the given collection of resources by index, plus any additional resources per the
* server's processing rules (e.g. _include'd resources, OperationOutcome, etc.). For example, * server's processing rules (e.g. _include'd resources, OperationOutcome, etc.). For example,
@ -128,10 +163,13 @@ public interface IBundleProvider {
* *
* @param theFromIndex The low index (inclusive) to return * @param theFromIndex The low index (inclusive) to return
* @param theToIndex The high index (exclusive) to return * @param theToIndex The high index (exclusive) to return
* @param theResponsePageBuilder The ResponsePageBuilder. The builder will add values needed for the response page.
* @return A list of resources. The size of this list must be at least <code>theToIndex - theFromIndex</code>. * @return A list of resources. The size of this list must be at least <code>theToIndex - theFromIndex</code>.
*/ */
@Nonnull default List<IBaseResource> getResources(int theFromIndex, int theToIndex, @Nonnull ResponsePage.ResponsePageBuilder theResponsePageBuilder) {
List<IBaseResource> getResources(int theFromIndex, int theToIndex); // TODO - override and implement
return getResources(theFromIndex, theToIndex);
}
/** /**
* Get all resources * Get all resources

View File

@ -19,6 +19,7 @@
*/ */
package ca.uhn.fhir.rest.server; package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.rest.server.method.ResponsePage;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
@ -85,9 +86,10 @@ public class BundleProviderWithNamedPages extends SimpleBundleProvider {
return this; return this;
} }
@SuppressWarnings("unchecked")
@Nonnull @Nonnull
@Override @Override
public List<IBaseResource> getResources(int theFromIndex, int theToIndex) { public List<IBaseResource> getResources(int theFromIndex, int theToIndex, @Nonnull ResponsePage.ResponsePageBuilder theResponsePageBuilder) {
return (List<IBaseResource>) getList(); // indexes are ignored for this provider type return (List<IBaseResource>) getList(); // indexes are ignored for this provider type
} }

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.server.method.ResponsePage;
import ca.uhn.fhir.util.CoverageIgnore; import ca.uhn.fhir.util.CoverageIgnore;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
@ -47,7 +48,7 @@ public class BundleProviders {
return new IBundleProvider() { return new IBundleProvider() {
@Nonnull @Nonnull
@Override @Override
public List<IBaseResource> getResources(int theFromIndex, int theToIndex) { public List<IBaseResource> getResources(int theFromIndex, int theToIndex, ResponsePage.ResponsePageBuilder theResponsePageBuilder) {
return Collections.emptyList(); return Collections.emptyList();
} }

View File

@ -21,15 +21,16 @@ package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.server.method.ResponsePage;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringBuilder;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.instance.model.api.IPrimitiveType;
import javax.annotation.Nonnull;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import javax.annotation.Nonnull;
public class SimpleBundleProvider implements IBundleProvider { public class SimpleBundleProvider implements IBundleProvider {
@ -40,6 +41,7 @@ public class SimpleBundleProvider implements IBundleProvider {
private IPrimitiveType<Date> myPublished = InstantDt.withCurrentTime(); private IPrimitiveType<Date> myPublished = InstantDt.withCurrentTime();
private Integer myCurrentPageOffset; private Integer myCurrentPageOffset;
private Integer myCurrentPageSize; private Integer myCurrentPageSize;
private ResponsePage.ResponsePageBuilder myPageBuilder;
/** /**
* Constructor * Constructor
@ -137,9 +139,10 @@ public class SimpleBundleProvider implements IBundleProvider {
myPublished = thePublished; myPublished = thePublished;
} }
@SuppressWarnings("unchecked")
@Nonnull @Nonnull
@Override @Override
public List<IBaseResource> getResources(int theFromIndex, int theToIndex) { public List<IBaseResource> getResources(int theFromIndex, int theToIndex, @Nonnull ResponsePage.ResponsePageBuilder theResponsePageBuilder) {
return (List<IBaseResource>) return (List<IBaseResource>)
myList.subList(Math.min(theFromIndex, myList.size()), Math.min(theToIndex, myList.size())); myList.subList(Math.min(theFromIndex, myList.size()), Math.min(theToIndex, myList.size()));
} }

View File

@ -181,8 +181,8 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
@Nonnull @Nonnull
@Override @Override
public List<IBaseResource> getResources(int theFromIndex, int theToIndex) { public List<IBaseResource> getResources(int theFromIndex, int theToIndex, ResponsePage.ResponsePageBuilder theResponsePageBuilder) {
List<IBaseResource> retVal = resources.getResources(theFromIndex, theToIndex); List<IBaseResource> retVal = resources.getResources(theFromIndex, theToIndex, theResponsePageBuilder);
int index = theFromIndex; int index = theFromIndex;
for (IBaseResource nextResource : retVal) { for (IBaseResource nextResource : retVal) {
if (nextResource.getIdElement() == null if (nextResource.getIdElement() == null

View File

@ -91,6 +91,8 @@ public class ResponseBundleBuilder {
final List<IBaseResource> resourceList; final List<IBaseResource> resourceList;
final int pageSize; final int pageSize;
ResponsePage.ResponsePageBuilder responsePageBuilder = bundleProvider.getResponsePageBuilder();
int numToReturn; int numToReturn;
String searchId = null; String searchId = null;
@ -98,24 +100,35 @@ public class ResponseBundleBuilder {
pageSize = offsetCalculatePageSize(server, requestedPage, bundleProvider.size()); pageSize = offsetCalculatePageSize(server, requestedPage, bundleProvider.size());
numToReturn = pageSize; numToReturn = pageSize;
resourceList = offsetBuildResourceList(bundleProvider, requestedPage, numToReturn); resourceList = offsetBuildResourceList(bundleProvider, requestedPage, numToReturn, responsePageBuilder);
RestfulServerUtils.validateResourceListNotNull(resourceList); RestfulServerUtils.validateResourceListNotNull(resourceList);
} else { } else {
pageSize = pagingCalculatePageSize(requestedPage, server.getPagingProvider()); pageSize = pagingCalculatePageSize(requestedPage, server.getPagingProvider());
if (bundleProvider.size() == null) { Integer size = bundleProvider.size();
if (size == null) {
numToReturn = pageSize; numToReturn = pageSize;
} else { } else {
numToReturn = Math.min(pageSize, bundleProvider.size() - theResponseBundleRequest.offset); numToReturn = Math.min(pageSize, size.intValue() - theResponseBundleRequest.offset);
} }
resourceList = pagingBuildResourceList(theResponseBundleRequest, bundleProvider, numToReturn); resourceList = pagingBuildResourceList(theResponseBundleRequest, bundleProvider, numToReturn, responsePageBuilder);
RestfulServerUtils.validateResourceListNotNull(resourceList); RestfulServerUtils.validateResourceListNotNull(resourceList);
searchId = pagingBuildSearchId(theResponseBundleRequest, numToReturn, bundleProvider.size()); searchId = pagingBuildSearchId(theResponseBundleRequest, numToReturn, bundleProvider.size());
} }
return new ResponsePage(searchId, resourceList, pageSize, numToReturn, bundleProvider.size()); // We should leave the IBundleProvider to populate these values (specifically resourceList).
// But since we haven't updated all such providers, we will
// build it here (this is at best 'duplicating' work).
responsePageBuilder
.setSearchId(searchId)
.setPageSize(pageSize)
.setNumToReturn(numToReturn)
.setNumTotalResults(bundleProvider.size())
.setResources(resourceList);
return responsePageBuilder.build();
} }
private static String pagingBuildSearchId( private static String pagingBuildSearchId(
@ -141,11 +154,18 @@ public class ResponseBundleBuilder {
} }
private static List<IBaseResource> pagingBuildResourceList( private static List<IBaseResource> pagingBuildResourceList(
ResponseBundleRequest theResponseBundleRequest, IBundleProvider theBundleProvider, int theNumToReturn) { ResponseBundleRequest theResponseBundleRequest,
IBundleProvider theBundleProvider,
int theNumToReturn,
ResponsePage.ResponsePageBuilder theResponsePageBuilder
) {
final List<IBaseResource> retval; final List<IBaseResource> retval;
if (theNumToReturn > 0 || theBundleProvider.getCurrentPageId() != null) { if (theNumToReturn > 0 || theBundleProvider.getCurrentPageId() != null) {
retval = theBundleProvider.getResources( retval = theBundleProvider.getResources(
theResponseBundleRequest.offset, theNumToReturn + theResponseBundleRequest.offset); theResponseBundleRequest.offset,
theNumToReturn + theResponseBundleRequest.offset,
theResponsePageBuilder
);
} else { } else {
retval = Collections.emptyList(); retval = Collections.emptyList();
} }
@ -161,15 +181,19 @@ public class ResponseBundleBuilder {
} }
private List<IBaseResource> offsetBuildResourceList( private List<IBaseResource> offsetBuildResourceList(
IBundleProvider theBundleProvider, RequestedPage theRequestedPage, int theNumToReturn) { IBundleProvider theBundleProvider,
RequestedPage theRequestedPage,
int theNumToReturn,
ResponsePage.ResponsePageBuilder theResponsePageBuilder
) {
final List<IBaseResource> retval; final List<IBaseResource> retval;
if ((theRequestedPage.offset != null && !myIsOffsetModeHistory) if ((theRequestedPage.offset != null && !myIsOffsetModeHistory)
|| theBundleProvider.getCurrentPageOffset() != null) { || theBundleProvider.getCurrentPageOffset() != null) {
// When offset query is done theResult already contains correct amount (+ their includes etc.) so return // When offset query is done theResult already contains correct amount (+ their includes etc.) so return
// everything // everything
retval = theBundleProvider.getResources(0, Integer.MAX_VALUE); retval = theBundleProvider.getResources(0, Integer.MAX_VALUE, theResponsePageBuilder);
} else if (theNumToReturn > 0) { } else if (theNumToReturn > 0) {
retval = theBundleProvider.getResources(0, theNumToReturn); retval = theBundleProvider.getResources(0, theNumToReturn, theResponsePageBuilder);
} else { } else {
retval = Collections.emptyList(); retval = Collections.emptyList();
} }
@ -177,7 +201,10 @@ public class ResponseBundleBuilder {
} }
private static int offsetCalculatePageSize( private static int offsetCalculatePageSize(
IRestfulServer<?> server, RequestedPage theRequestedPage, Integer theNumTotalResults) { IRestfulServer<?> server,
RequestedPage theRequestedPage,
Integer theNumTotalResults
) {
final int retval; final int retval;
if (theRequestedPage.limit != null) { if (theRequestedPage.limit != null) {
retval = theRequestedPage.limit; retval = theRequestedPage.limit;
@ -271,7 +298,6 @@ public class ResponseBundleBuilder {
} }
if (theResponsePage.numTotalResults == null || requestedToReturn < theResponsePage.numTotalResults) { if (theResponsePage.numTotalResults == null || requestedToReturn < theResponsePage.numTotalResults) {
retval.setNext(RestfulServerUtils.createOffsetPagingLink( retval.setNext(RestfulServerUtils.createOffsetPagingLink(
retval, retval,
theResponseBundleRequest.requestDetails.getRequestPath(), theResponseBundleRequest.requestDetails.getRequestPath(),

View File

@ -49,20 +49,83 @@ public class ResponsePage {
*/ */
public final int numToReturn; public final int numToReturn;
public ResponsePage( /**
* The count of resources included from the _include filter.
* These _include resources are otherwise included in the resourceList.
*/
private final int includedResourceCount;
ResponsePage(
String theSearchId, String theSearchId,
List<IBaseResource> theResourceList, List<IBaseResource> theResourceList,
int thePageSize, int thePageSize,
int theNumToReturn, int theNumToReturn,
Integer theNumTotalResults) { Integer theNumTotalResults,
int theIncludedResourceCount
) {
searchId = theSearchId; searchId = theSearchId;
resourceList = theResourceList; resourceList = theResourceList;
pageSize = thePageSize; pageSize = thePageSize;
numToReturn = theNumToReturn; numToReturn = theNumToReturn;
numTotalResults = theNumTotalResults; numTotalResults = theNumTotalResults;
includedResourceCount = theIncludedResourceCount;
} }
public int size() { public int size() {
return resourceList.size(); return resourceList.size();
} }
/**
* A builder for constructing ResponsePage objects.
*/
public static class ResponsePageBuilder {
private String mySearchId;
private List<IBaseResource> myResources;
private Integer myNumTotalResults;
private int myPageSize;
private int myNumToReturn;
private int myIncludedResourceCount;
public ResponsePageBuilder setIncludedResourceCount(int theIncludedResourceCount) {
myIncludedResourceCount = theIncludedResourceCount;
return this;
}
public ResponsePageBuilder setNumToReturn(int theNumToReturn) {
myNumToReturn = theNumToReturn;
return this;
}
public ResponsePageBuilder setPageSize(int thePageSize) {
myPageSize = thePageSize;
return this;
}
public ResponsePageBuilder setNumTotalResults(Integer theNumTotalResults) {
myNumTotalResults = theNumTotalResults;
return this;
}
public ResponsePageBuilder setResources(List<IBaseResource> theResources) {
myResources = theResources;
return this;
}
public ResponsePageBuilder setSearchId(String theSearchId) {
mySearchId = theSearchId;
return this;
}
public ResponsePage build() {
return new ResponsePage(
mySearchId, // search id
myResources, // resource list
myPageSize, // page size
myNumToReturn, // num to return
myNumTotalResults, // total results
myIncludedResourceCount // included count
);
}
}
} }

View File

@ -53,6 +53,7 @@ import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.SimpleBundleProvider; import ca.uhn.fhir.rest.server.SimpleBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.method.ResponsePage;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.ValidateUtil; import ca.uhn.fhir.util.ValidateUtil;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
@ -64,6 +65,7 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@ -76,7 +78,6 @@ import java.util.Map;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import static java.lang.Math.max; import static java.lang.Math.max;
import static java.lang.Math.min; import static java.lang.Math.min;
@ -319,7 +320,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Nonnull @Nonnull
@Override @Override
public List<IBaseResource> getResources(int theFromIndex, int theToIndex) { public List<IBaseResource> getResources(int theFromIndex, int theToIndex, @Nonnull ResponsePage.ResponsePageBuilder theResponsePageBuilder) {
// Make sure that "from" isn't less than 0, "to" isn't more than the number available, // Make sure that "from" isn't less than 0, "to" isn't more than the number available,
// and "from" <= "to" // and "from" <= "to"

View File

@ -13,6 +13,7 @@ import ca.uhn.fhir.rest.server.SimpleBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -150,6 +151,55 @@ class ResponseBundleBuilderTest {
assertNextLink(bundle, DEFAULT_PAGE_SIZE); assertNextLink(bundle, DEFAULT_PAGE_SIZE);
} }
@Test
public void buildResponseBundle_withIncludeFilterAndFewerResultsThanPageSize_doesNotReturnNextLink() {
// setup
int includeResources = 4;
// we want the number of resources returned to be equal to the pagesize
List<IBaseResource> list = buildXPatientList(DEFAULT_PAGE_SIZE + 1 - includeResources);
ResponsePage.ResponsePageBuilder builder = new ResponsePage.ResponsePageBuilder();
builder.setIncludedResourceCount(includeResources);
ResponseBundleBuilder svc = new ResponseBundleBuilder(true);
SimpleBundleProvider provider = new SimpleBundleProvider() {
@Override
public ResponsePage.ResponsePageBuilder getResponsePageBuilder() {
return builder;
}
@Nonnull
@Override
public List<IBaseResource> getResources(int theFrom, int theTo, @Nonnull ResponsePage.ResponsePageBuilder theResponsePageBuilder) {
List<IBaseResource> retList = list.subList(theFrom, Math.min(theTo, list.size()-1));
// our fake includes
for (int i = 0; i < includeResources; i++) {
retList.add(new Organization().setId("Organization/" + i));
}
return retList;
}
};
// TODO - if null, it adds a next link
provider.setSize(DEFAULT_PAGE_SIZE);
// mocking
when(myServer.canStoreSearchResults()).thenReturn(true);
when(myServer.getPagingProvider()).thenReturn(myPagingProvider);
when(myPagingProvider.getDefaultPageSize()).thenReturn(DEFAULT_PAGE_SIZE);
ResponseBundleRequest req = buildResponseBundleRequest(provider);
// test
Bundle bundle = (Bundle) svc.buildResponseBundle(req);
// verify
assertEquals(1, bundle.getLink().size());
verifyBundle(bundle, RESOURCE_COUNT, DEFAULT_PAGE_SIZE -1, "A0", "A14");
}
@ParameterizedTest @ParameterizedTest
@ValueSource(booleans = {true, false}) @ValueSource(booleans = {true, false})
void testFilterNulls(boolean theCanStoreSearchResults) { void testFilterNulls(boolean theCanStoreSearchResults) {
@ -423,8 +473,12 @@ class ResponseBundleBuilderTest {
} }
private List<IBaseResource> buildPatientList() { private List<IBaseResource> buildPatientList() {
return buildXPatientList(ResponseBundleBuilderTest.RESOURCE_COUNT);
}
private List<IBaseResource> buildXPatientList(int theCount) {
List<IBaseResource> retval = new ArrayList<>(); List<IBaseResource> retval = new ArrayList<>();
for (int i = 0; i < ResponseBundleBuilderTest.RESOURCE_COUNT; ++i) { for (int i = 0; i < theCount; ++i) {
Patient p = new Patient(); Patient p = new Patient();
p.setId("A" + i); p.setId("A" + i);
p.setActive(true); p.setActive(true);
@ -499,10 +553,9 @@ class ResponseBundleBuilderTest {
} }
@Nonnull @Nonnull
@Override public List<IBaseResource> getResources(int theFromIndex, int theToIndex, @Nonnull ResponsePage.ResponsePageBuilder theResponseBundleBuilder) {
public List<IBaseResource> getResources(int theFromIndex, int theToIndex) {
getResourcesCalled = true; getResourcesCalled = true;
return super.getResources(theFromIndex, theToIndex); return super.getResources(theFromIndex, theToIndex, theResponseBundleBuilder);
} }
// Emulate the behaviour of PersistedJpaBundleProvider where size() is only set after getResources() has been called // Emulate the behaviour of PersistedJpaBundleProvider where size() is only set after getResources() has been called