mirror of
https://github.com/hapifhir/hapi-fhir.git
synced 2025-02-18 02:45:07 +00:00
Add forced offset search JPA interceptor (#2756)
* Add support for offset searches * Tests working * Add changelog * Test fixes * Test fix * More test fixes * Add commit counter
This commit is contained in:
parent
c64269054b
commit
7185089c9d
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
type: add
|
||||||
|
issue: 2756
|
||||||
|
title: "A new interceptor has been addeed to the JPA server called `ForceOffsetSearchModeInterceptor`. This
|
||||||
|
interceptor forces all searches to be offset searches, instead of relying on the query cache. This means
|
||||||
|
that FHIR search operations will never result in any database write, which can be good for highly
|
||||||
|
concurrent servers."
|
@ -145,11 +145,17 @@ public class Search implements ICachedSearchDetails, Serializable {
|
|||||||
private byte[] mySearchParameterMap;
|
private byte[] mySearchParameterMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This isn't currently persisted in the DB as it's only used for history. We could
|
* This isn't currently persisted in the DB as it's only used for offset mode. We could
|
||||||
* change this if needed in the future.
|
* change this if needed in the future.
|
||||||
*/
|
*/
|
||||||
@Transient
|
@Transient
|
||||||
private Integer myOffset;
|
private Integer myOffset;
|
||||||
|
/**
|
||||||
|
* This isn't currently persisted in the DB as it's only used for offset mode. We could
|
||||||
|
* change this if needed in the future.
|
||||||
|
*/
|
||||||
|
@Transient
|
||||||
|
private Integer mySizeModeSize;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
@ -158,6 +164,10 @@ public class Search implements ICachedSearchDetails, Serializable {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer getSizeModeSize() {
|
||||||
|
return mySizeModeSize;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return new ToStringBuilder(this)
|
return new ToStringBuilder(this)
|
||||||
@ -396,14 +406,14 @@ public class Search implements ICachedSearchDetails, Serializable {
|
|||||||
mySearchQueryStringHash = null;
|
mySearchQueryStringHash = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOffset(Integer theOffset) {
|
|
||||||
myOffset = theOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getOffset() {
|
public Integer getOffset() {
|
||||||
return myOffset;
|
return myOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setOffset(Integer theOffset) {
|
||||||
|
myOffset = theOffset;
|
||||||
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
public static String createSearchQueryStringForStorage(@Nonnull String theSearchQueryString, @Nonnull RequestPartitionId theRequestPartitionId) {
|
public static String createSearchQueryStringForStorage(@Nonnull String theSearchQueryString, @Nonnull RequestPartitionId theRequestPartitionId) {
|
||||||
String searchQueryString = theSearchQueryString;
|
String searchQueryString = theSearchQueryString;
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
package ca.uhn.fhir.jpa.interceptor;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR JPA Server
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ca.uhn.fhir.interceptor.api.Hook;
|
||||||
|
import ca.uhn.fhir.interceptor.api.Interceptor;
|
||||||
|
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||||
|
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||||
|
import org.apache.commons.lang3.Validate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This interceptor for the HAPI FHIR JPA server forces all queries to
|
||||||
|
* be performed as offset queries. This means that the query cache will
|
||||||
|
* not be used and searches will never result in any writes to the
|
||||||
|
* database.
|
||||||
|
*/
|
||||||
|
@Interceptor
|
||||||
|
public class ForceOffsetSearchModeInterceptor {
|
||||||
|
|
||||||
|
private Integer myDefaultCount = 100;
|
||||||
|
|
||||||
|
public void setDefaultCount(Integer theDefaultCount) {
|
||||||
|
Validate.notNull(theDefaultCount, "theDefaultCount must not be null");
|
||||||
|
myDefaultCount = theDefaultCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Hook(Pointcut.STORAGE_PRESEARCH_REGISTERED)
|
||||||
|
public void storagePreSearchRegistered(SearchParameterMap theMap) {
|
||||||
|
if (theMap.getOffset() == null) {
|
||||||
|
theMap.setOffset(0);
|
||||||
|
}
|
||||||
|
if (theMap.getCount() == null) {
|
||||||
|
theMap.setCount(myDefaultCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -307,6 +307,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
|
|||||||
|
|
||||||
final String queryString = theParams.toNormalizedQueryString(myContext);
|
final String queryString = theParams.toNormalizedQueryString(myContext);
|
||||||
ourLog.debug("Registering new search {}", searchUuid);
|
ourLog.debug("Registering new search {}", searchUuid);
|
||||||
|
|
||||||
Search search = new Search();
|
Search search = new Search();
|
||||||
populateSearchEntity(theParams, theResourceType, searchUuid, queryString, search, theRequestPartitionId);
|
populateSearchEntity(theParams, theResourceType, searchUuid, queryString, search, theRequestPartitionId);
|
||||||
|
|
||||||
@ -317,13 +318,15 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
|
|||||||
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
|
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
|
||||||
.add(SearchParameterMap.class, theParams);
|
.add(SearchParameterMap.class, theParams);
|
||||||
CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PRESEARCH_REGISTERED, params);
|
CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PRESEARCH_REGISTERED, params);
|
||||||
|
|
||||||
Class<? extends IBaseResource> resourceTypeClass = myContext.getResourceDefinition(theResourceType).getImplementingClass();
|
Class<? extends IBaseResource> resourceTypeClass = myContext.getResourceDefinition(theResourceType).getImplementingClass();
|
||||||
final ISearchBuilder sb = mySearchBuilderFactory.newSearchBuilder(theCallingDao, theResourceType, resourceTypeClass);
|
final ISearchBuilder sb = mySearchBuilderFactory.newSearchBuilder(theCallingDao, theResourceType, resourceTypeClass);
|
||||||
sb.setFetchSize(mySyncSize);
|
sb.setFetchSize(mySyncSize);
|
||||||
|
|
||||||
final Integer loadSynchronousUpTo = getLoadSynchronousUpToOrNull(theCacheControlDirective);
|
final Integer loadSynchronousUpTo = getLoadSynchronousUpToOrNull(theCacheControlDirective);
|
||||||
|
boolean isOffsetQuery = theParams.isOffsetQuery();
|
||||||
|
|
||||||
if (theParams.isLoadSynchronous() || loadSynchronousUpTo != null) {
|
if (theParams.isLoadSynchronous() || loadSynchronousUpTo != null || isOffsetQuery) {
|
||||||
ourLog.debug("Search {} is loading in synchronous mode", searchUuid);
|
ourLog.debug("Search {} is loading in synchronous mode", searchUuid);
|
||||||
return executeQuery(theResourceType, theParams, theRequestDetails, searchUuid, sb, loadSynchronousUpTo, theRequestPartitionId);
|
return executeQuery(theResourceType, theParams, theRequestDetails, searchUuid, sb, loadSynchronousUpTo, theRequestPartitionId);
|
||||||
}
|
}
|
||||||
@ -466,6 +469,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
|
|||||||
// Execute the query and make sure we return distinct results
|
// Execute the query and make sure we return distinct results
|
||||||
TransactionTemplate txTemplate = new TransactionTemplate(myManagedTxManager);
|
TransactionTemplate txTemplate = new TransactionTemplate(myManagedTxManager);
|
||||||
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
|
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
|
||||||
|
txTemplate.setReadOnly(theParams.isLoadSynchronous() || theParams.isOffsetQuery());
|
||||||
return txTemplate.execute(t -> {
|
return txTemplate.execute(t -> {
|
||||||
|
|
||||||
// Load the results synchronously
|
// Load the results synchronously
|
||||||
@ -554,6 +558,10 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
|
|||||||
resources = InterceptorUtil.fireStoragePreshowResource(resources, theRequestDetails, myInterceptorBroadcaster);
|
resources = InterceptorUtil.fireStoragePreshowResource(resources, theRequestDetails, myInterceptorBroadcaster);
|
||||||
|
|
||||||
SimpleBundleProvider bundleProvider = new SimpleBundleProvider(resources);
|
SimpleBundleProvider bundleProvider = new SimpleBundleProvider(resources);
|
||||||
|
if (theParams.isOffsetQuery()) {
|
||||||
|
bundleProvider.setCurrentPageOffset(theParams.getOffset());
|
||||||
|
bundleProvider.setCurrentPageSize(theParams.getCount());
|
||||||
|
}
|
||||||
|
|
||||||
if (wantCount) {
|
if (wantCount) {
|
||||||
bundleProvider.setSize(count.intValue());
|
bundleProvider.setSize(count.intValue());
|
||||||
|
@ -22,17 +22,20 @@ package ca.uhn.fhir.jpa.util;
|
|||||||
|
|
||||||
import net.ttddyy.dsproxy.ExecutionInfo;
|
import net.ttddyy.dsproxy.ExecutionInfo;
|
||||||
import net.ttddyy.dsproxy.QueryInfo;
|
import net.ttddyy.dsproxy.QueryInfo;
|
||||||
|
import net.ttddyy.dsproxy.listener.MethodExecutionContext;
|
||||||
import net.ttddyy.dsproxy.proxy.ParameterSetOperation;
|
import net.ttddyy.dsproxy.proxy.ParameterSetOperation;
|
||||||
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
|
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.apache.commons.lang3.StringUtils.trim;
|
import static org.apache.commons.lang3.StringUtils.trim;
|
||||||
|
|
||||||
public abstract class BaseCaptureQueriesListener implements ProxyDataSourceBuilder.SingleQueryExecution {
|
public abstract class BaseCaptureQueriesListener implements ProxyDataSourceBuilder.SingleQueryExecution, ProxyDataSourceBuilder.SingleMethodExecution {
|
||||||
|
|
||||||
private boolean myCaptureQueryStackTrace = false;
|
private boolean myCaptureQueryStackTrace = false;
|
||||||
|
|
||||||
@ -88,4 +91,34 @@ public abstract class BaseCaptureQueriesListener implements ProxyDataSourceBuild
|
|||||||
|
|
||||||
protected abstract Queue<SqlQuery> provideQueryList();
|
protected abstract Queue<SqlQuery> provideQueryList();
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
protected abstract AtomicInteger provideCommitCounter();
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
protected abstract AtomicInteger provideRollbackCounter();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(MethodExecutionContext executionContext) {
|
||||||
|
AtomicInteger counter = null;
|
||||||
|
switch (executionContext.getMethod().getName()) {
|
||||||
|
case "commit":
|
||||||
|
counter = provideCommitCounter();
|
||||||
|
break;
|
||||||
|
case "rollback":
|
||||||
|
counter = provideRollbackCounter();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (counter != null) {
|
||||||
|
counter.incrementAndGet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int countCommits() {
|
||||||
|
return provideCommitCounter().get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int countRollbacks() {
|
||||||
|
return provideRollbackCounter().get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,7 @@ import java.util.Collections;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
@ -52,6 +53,8 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe
|
|||||||
private static final int CAPACITY = 1000;
|
private static final int CAPACITY = 1000;
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(CircularQueueCaptureQueriesListener.class);
|
private static final Logger ourLog = LoggerFactory.getLogger(CircularQueueCaptureQueriesListener.class);
|
||||||
private Queue<SqlQuery> myQueries;
|
private Queue<SqlQuery> myQueries;
|
||||||
|
private AtomicInteger myCommitCounter;
|
||||||
|
private AtomicInteger myRollbackCounter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
@ -65,11 +68,23 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe
|
|||||||
return myQueries;
|
return myQueries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AtomicInteger provideCommitCounter() {
|
||||||
|
return myCommitCounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AtomicInteger provideRollbackCounter() {
|
||||||
|
return myRollbackCounter;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all stored queries
|
* Clear all stored queries
|
||||||
*/
|
*/
|
||||||
public void clear() {
|
public void clear() {
|
||||||
myQueries.clear();
|
myQueries.clear();
|
||||||
|
myCommitCounter.set(0);
|
||||||
|
myRollbackCounter.set(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -77,6 +92,8 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe
|
|||||||
*/
|
*/
|
||||||
public void startCollecting() {
|
public void startCollecting() {
|
||||||
myQueries = Queues.synchronizedQueue(new CircularFifoQueue<>(CAPACITY));
|
myQueries = Queues.synchronizedQueue(new CircularFifoQueue<>(CAPACITY));
|
||||||
|
myCommitCounter = new AtomicInteger(0);
|
||||||
|
myRollbackCounter = new AtomicInteger(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -84,6 +101,8 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe
|
|||||||
*/
|
*/
|
||||||
public void stopCollecting() {
|
public void stopCollecting() {
|
||||||
myQueries = null;
|
myQueries = null;
|
||||||
|
myCommitCounter = null;
|
||||||
|
myRollbackCounter = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -115,6 +134,14 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe
|
|||||||
return getQueriesStartingWith(theStart, null);
|
return getQueriesStartingWith(theStart, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getCommitCount() {
|
||||||
|
return myCommitCounter.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRollbackCount() {
|
||||||
|
return myRollbackCounter.get();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all SELECT queries executed on the current thread - Index 0 is oldest
|
* Returns all SELECT queries executed on the current thread - Index 0 is oldest
|
||||||
*/
|
*/
|
||||||
|
@ -27,11 +27,14 @@ import java.util.ArrayDeque;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class CurrentThreadCaptureQueriesListener extends BaseCaptureQueriesListener {
|
public class CurrentThreadCaptureQueriesListener extends BaseCaptureQueriesListener {
|
||||||
|
|
||||||
private static final ThreadLocal<Queue<SqlQuery>> ourQueues = new ThreadLocal<>();
|
private static final ThreadLocal<Queue<SqlQuery>> ourQueues = new ThreadLocal<>();
|
||||||
|
private static final ThreadLocal<AtomicInteger> ourCommits = new ThreadLocal<>();
|
||||||
|
private static final ThreadLocal<AtomicInteger> ourRollbacks = new ThreadLocal<>();
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(CurrentThreadCaptureQueriesListener.class);
|
private static final Logger ourLog = LoggerFactory.getLogger(CurrentThreadCaptureQueriesListener.class);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -39,18 +42,31 @@ public class CurrentThreadCaptureQueriesListener extends BaseCaptureQueriesListe
|
|||||||
return ourQueues.get();
|
return ourQueues.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AtomicInteger provideCommitCounter() {
|
||||||
|
return ourCommits.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AtomicInteger provideRollbackCounter() {
|
||||||
|
return ourRollbacks.get();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current queue of items and stop collecting
|
* Get the current queue of items and stop collecting
|
||||||
*/
|
*/
|
||||||
public static SqlQueryList getCurrentQueueAndStopCapturing() {
|
public static SqlQueryList getCurrentQueueAndStopCapturing() {
|
||||||
Queue<SqlQuery> retVal = ourQueues.get();
|
Queue<SqlQuery> retVal = ourQueues.get();
|
||||||
ourQueues.remove();
|
ourQueues.remove();
|
||||||
|
ourCommits.remove();
|
||||||
|
ourRollbacks.remove();
|
||||||
if (retVal == null) {
|
if (retVal == null) {
|
||||||
return new SqlQueryList();
|
return new SqlQueryList();
|
||||||
}
|
}
|
||||||
return new SqlQueryList(retVal);
|
return new SqlQueryList(retVal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts capturing queries for the current thread.
|
* Starts capturing queries for the current thread.
|
||||||
* <p>
|
* <p>
|
||||||
@ -62,6 +78,8 @@ public class CurrentThreadCaptureQueriesListener extends BaseCaptureQueriesListe
|
|||||||
*/
|
*/
|
||||||
public static void startCapturing() {
|
public static void startCapturing() {
|
||||||
ourQueues.set(new ArrayDeque<>());
|
ourQueues.set(new ArrayDeque<>());
|
||||||
|
ourCommits.set(new AtomicInteger(0));
|
||||||
|
ourRollbacks.set(new AtomicInteger(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -127,6 +127,7 @@ public class TestR4Config extends BaseJavaConfigR4 {
|
|||||||
.afterQuery(captureQueriesListener())
|
.afterQuery(captureQueriesListener())
|
||||||
.afterQuery(new CurrentThreadCaptureQueriesListener())
|
.afterQuery(new CurrentThreadCaptureQueriesListener())
|
||||||
.countQuery(singleQueryCountHolder())
|
.countQuery(singleQueryCountHolder())
|
||||||
|
.afterMethod(captureQueriesListener())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
return dataSource;
|
return dataSource;
|
||||||
|
@ -5,11 +5,13 @@ import ca.uhn.fhir.jpa.api.model.HistoryCountModeEnum;
|
|||||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||||
import ca.uhn.fhir.jpa.model.util.JpaConstants;
|
import ca.uhn.fhir.jpa.model.util.JpaConstants;
|
||||||
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
|
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
|
||||||
|
import ca.uhn.fhir.jpa.provider.r4.BaseResourceProviderR4Test;
|
||||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||||
import ca.uhn.fhir.jpa.util.SqlQuery;
|
import ca.uhn.fhir.jpa.util.SqlQuery;
|
||||||
import ca.uhn.fhir.rest.api.SortSpec;
|
import ca.uhn.fhir.rest.api.SortSpec;
|
||||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||||
|
import ca.uhn.fhir.rest.param.TokenParam;
|
||||||
import ca.uhn.fhir.util.BundleBuilder;
|
import ca.uhn.fhir.util.BundleBuilder;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.hl7.fhir.instance.model.api.IIdType;
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
@ -46,10 +48,11 @@ import static org.hamcrest.Matchers.containsInAnyOrder;
|
|||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.hamcrest.Matchers.empty;
|
import static org.hamcrest.Matchers.empty;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
|
public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test {
|
||||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4QueryCountTest.class);
|
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4QueryCountTest.class);
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
@ -66,8 +69,10 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
|
|||||||
myDaoConfig.setTagStorageMode(new DaoConfig().getTagStorageMode());
|
myDaoConfig.setTagStorageMode(new DaoConfig().getTagStorageMode());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void before() {
|
public void before() throws Exception {
|
||||||
|
super.before();
|
||||||
myInterceptorRegistry.registerInterceptor(myInterceptor);
|
myInterceptorRegistry.registerInterceptor(myInterceptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -534,6 +539,92 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearchUsingOffsetMode_Explicit() {
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
createPatient(withId("A" + i), withActiveTrue());
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchParameterMap map = new SearchParameterMap();
|
||||||
|
map.setLoadSynchronousUpTo(5);
|
||||||
|
map.setOffset(0);
|
||||||
|
map.add("active", new TokenParam("true"));
|
||||||
|
|
||||||
|
// First page
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
Bundle outcome = myClient
|
||||||
|
.search()
|
||||||
|
.forResource("Patient")
|
||||||
|
.where(Patient.ACTIVE.exactly().code("true"))
|
||||||
|
.offset(0)
|
||||||
|
.count(5)
|
||||||
|
.returnBundle(Bundle.class)
|
||||||
|
.execute();
|
||||||
|
assertThat(toUnqualifiedVersionlessIdValues(outcome).toString(), toUnqualifiedVersionlessIdValues(outcome), containsInAnyOrder(
|
||||||
|
"Patient/A0", "Patient/A1", "Patient/A2", "Patient/A3", "Patient/A4"
|
||||||
|
));
|
||||||
|
myCaptureQueriesListener.logSelectQueries();
|
||||||
|
assertEquals(2, myCaptureQueriesListener.countSelectQueries());
|
||||||
|
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false), containsString("SELECT t0.RES_ID FROM HFJ_SPIDX_TOKEN t0"));
|
||||||
|
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false), containsString("limit '5'"));
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countInsertQueries());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countUpdateQueries());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||||
|
assertEquals(1, myCaptureQueriesListener.countCommits());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countRollbacks());
|
||||||
|
|
||||||
|
assertThat(outcome.getLink("next").getUrl(), containsString("Patient?_count=5&_offset=5&active=true"));
|
||||||
|
|
||||||
|
// Second page
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
outcome = myClient
|
||||||
|
.search()
|
||||||
|
.forResource("Patient")
|
||||||
|
.where(Patient.ACTIVE.exactly().code("true"))
|
||||||
|
.offset(5)
|
||||||
|
.count(5)
|
||||||
|
.returnBundle(Bundle.class)
|
||||||
|
.execute();
|
||||||
|
assertThat(toUnqualifiedVersionlessIdValues(outcome).toString(), toUnqualifiedVersionlessIdValues(outcome), containsInAnyOrder(
|
||||||
|
"Patient/A5", "Patient/A6", "Patient/A7", "Patient/A8", "Patient/A9"
|
||||||
|
));
|
||||||
|
myCaptureQueriesListener.logSelectQueries();
|
||||||
|
assertEquals(2, myCaptureQueriesListener.countSelectQueries());
|
||||||
|
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false), containsString("SELECT t0.RES_ID FROM HFJ_SPIDX_TOKEN t0"));
|
||||||
|
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false), containsString("limit '5'"));
|
||||||
|
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false), containsString("offset '5'"));
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countInsertQueries());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countUpdateQueries());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||||
|
assertEquals(1, myCaptureQueriesListener.countCommits());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countRollbacks());
|
||||||
|
|
||||||
|
assertThat(outcome.getLink("next").getUrl(), containsString("Patient?_count=5&_offset=10&active=true"));
|
||||||
|
|
||||||
|
// Third page (no results)
|
||||||
|
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
outcome = myClient
|
||||||
|
.search()
|
||||||
|
.forResource("Patient")
|
||||||
|
.where(Patient.ACTIVE.exactly().code("true"))
|
||||||
|
.offset(10)
|
||||||
|
.count(5)
|
||||||
|
.returnBundle(Bundle.class)
|
||||||
|
.execute();
|
||||||
|
assertThat(toUnqualifiedVersionlessIdValues(outcome).toString(), toUnqualifiedVersionlessIdValues(outcome), empty());
|
||||||
|
myCaptureQueriesListener.logSelectQueries();
|
||||||
|
assertEquals(1, myCaptureQueriesListener.countSelectQueries());
|
||||||
|
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false), containsString("SELECT t0.RES_ID FROM HFJ_SPIDX_TOKEN t0"));
|
||||||
|
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false), containsString("limit '5'"));
|
||||||
|
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false), containsString("offset '10'"));
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countInsertQueries());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countUpdateQueries());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSearchUsingForcedIdReference() {
|
public void testSearchUsingForcedIdReference() {
|
||||||
|
|
||||||
|
@ -0,0 +1,158 @@
|
|||||||
|
package ca.uhn.fhir.jpa.interceptor;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.jpa.provider.r4.BaseResourceProviderR4Test;
|
||||||
|
import org.hl7.fhir.r4.model.Bundle;
|
||||||
|
import org.hl7.fhir.r4.model.Patient;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.hamcrest.Matchers.empty;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
|
public class ForceOffsetSearchModeInterceptorTest extends BaseResourceProviderR4Test {
|
||||||
|
|
||||||
|
private ForceOffsetSearchModeInterceptor mySvc;
|
||||||
|
private Integer myInitialDefaultPageSize;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@BeforeEach
|
||||||
|
public void before() throws Exception {
|
||||||
|
super.before();
|
||||||
|
|
||||||
|
mySvc = new ForceOffsetSearchModeInterceptor();
|
||||||
|
ourRestServer.registerInterceptor(mySvc);
|
||||||
|
myInitialDefaultPageSize = ourRestServer.getDefaultPageSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@AfterEach
|
||||||
|
public void after() throws Exception {
|
||||||
|
super.after();
|
||||||
|
|
||||||
|
ourRestServer.unregisterInterceptor(mySvc);
|
||||||
|
ourRestServer.setDefaultPageSize(myInitialDefaultPageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearch_NoExplcitCount() {
|
||||||
|
ourRestServer.setDefaultPageSize(5);
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
createPatient(withId("A" + i), withActiveTrue());
|
||||||
|
}
|
||||||
|
|
||||||
|
// First page
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
Bundle outcome = myClient
|
||||||
|
.search()
|
||||||
|
.forResource("Patient")
|
||||||
|
.where(Patient.ACTIVE.exactly().code("true"))
|
||||||
|
.returnBundle(Bundle.class)
|
||||||
|
.execute();
|
||||||
|
assertThat(toUnqualifiedVersionlessIdValues(outcome).toString(), toUnqualifiedVersionlessIdValues(outcome), containsInAnyOrder(
|
||||||
|
"Patient/A0", "Patient/A1", "Patient/A2", "Patient/A3", "Patient/A4"
|
||||||
|
));
|
||||||
|
myCaptureQueriesListener.logSelectQueries();
|
||||||
|
assertEquals(2, myCaptureQueriesListener.countSelectQueries());
|
||||||
|
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false), containsString("SELECT t0.RES_ID FROM HFJ_SPIDX_TOKEN t0"));
|
||||||
|
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false), containsString("limit '5'"));
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countInsertQueries());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countUpdateQueries());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||||
|
assertEquals(1, myCaptureQueriesListener.countCommits());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countRollbacks());
|
||||||
|
|
||||||
|
assertThat(outcome.getLink("next").getUrl(), containsString("Patient?_count=5&_offset=5&active=true"));
|
||||||
|
|
||||||
|
// Second page
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
outcome = myClient
|
||||||
|
.search()
|
||||||
|
.forResource("Patient")
|
||||||
|
.where(Patient.ACTIVE.exactly().code("true"))
|
||||||
|
.offset(5)
|
||||||
|
.count(5)
|
||||||
|
.returnBundle(Bundle.class)
|
||||||
|
.execute();
|
||||||
|
assertThat(toUnqualifiedVersionlessIdValues(outcome).toString(), toUnqualifiedVersionlessIdValues(outcome), containsInAnyOrder(
|
||||||
|
"Patient/A5", "Patient/A6", "Patient/A7", "Patient/A8", "Patient/A9"
|
||||||
|
));
|
||||||
|
myCaptureQueriesListener.logSelectQueries();
|
||||||
|
assertEquals(2, myCaptureQueriesListener.countSelectQueries());
|
||||||
|
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false), containsString("SELECT t0.RES_ID FROM HFJ_SPIDX_TOKEN t0"));
|
||||||
|
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false), containsString("limit '5'"));
|
||||||
|
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false), containsString("offset '5'"));
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countInsertQueries());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countUpdateQueries());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||||
|
assertEquals(1, myCaptureQueriesListener.countCommits());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countRollbacks());
|
||||||
|
|
||||||
|
assertThat(outcome.getLink("next").getUrl(), containsString("Patient?_count=5&_offset=10&active=true"));
|
||||||
|
|
||||||
|
// Third page (no results)
|
||||||
|
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
Bundle outcome3 = myClient
|
||||||
|
.search()
|
||||||
|
.forResource("Patient")
|
||||||
|
.where(Patient.ACTIVE.exactly().code("true"))
|
||||||
|
.offset(10)
|
||||||
|
.count(5)
|
||||||
|
.returnBundle(Bundle.class)
|
||||||
|
.execute();
|
||||||
|
assertThat(toUnqualifiedVersionlessIdValues(outcome3).toString(), toUnqualifiedVersionlessIdValues(outcome3), empty());
|
||||||
|
myCaptureQueriesListener.logSelectQueries();
|
||||||
|
assertEquals(1, myCaptureQueriesListener.countSelectQueries());
|
||||||
|
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false), containsString("SELECT t0.RES_ID FROM HFJ_SPIDX_TOKEN t0"));
|
||||||
|
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false), containsString("limit '5'"));
|
||||||
|
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false), containsString("offset '10'"));
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countInsertQueries());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countUpdateQueries());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||||
|
|
||||||
|
assertNull(outcome3.getLink("next"), () -> outcome3.getLink("next").getUrl());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearch_WithExplicitCount() {
|
||||||
|
ourRestServer.setDefaultPageSize(5);
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
createPatient(withId("A" + i), withActiveTrue());
|
||||||
|
}
|
||||||
|
|
||||||
|
// First page
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
Bundle outcome = myClient
|
||||||
|
.search()
|
||||||
|
.forResource("Patient")
|
||||||
|
.where(Patient.ACTIVE.exactly().code("true"))
|
||||||
|
.count(7)
|
||||||
|
.returnBundle(Bundle.class)
|
||||||
|
.execute();
|
||||||
|
assertThat(toUnqualifiedVersionlessIdValues(outcome).toString(), toUnqualifiedVersionlessIdValues(outcome), containsInAnyOrder(
|
||||||
|
"Patient/A0", "Patient/A1", "Patient/A2", "Patient/A3", "Patient/A4", "Patient/A5", "Patient/A6"
|
||||||
|
));
|
||||||
|
myCaptureQueriesListener.logSelectQueries();
|
||||||
|
assertEquals(2, myCaptureQueriesListener.countSelectQueries());
|
||||||
|
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false), containsString("SELECT t0.RES_ID FROM HFJ_SPIDX_TOKEN t0"));
|
||||||
|
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false), containsString("limit '7'"));
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countInsertQueries());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countUpdateQueries());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||||
|
assertEquals(1, myCaptureQueriesListener.countCommits());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countRollbacks());
|
||||||
|
|
||||||
|
assertThat(outcome.getLink("next").getUrl(), containsString("Patient?_count=7&_offset=7&active=true"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -631,6 +631,15 @@ public class SearchParameterMap implements Serializable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if {@link #getOffset()} and {@link #getCount()} both return a non null response
|
||||||
|
*
|
||||||
|
* @since 5.5.0
|
||||||
|
*/
|
||||||
|
public boolean isOffsetQuery() {
|
||||||
|
return getOffset() != null && getCount() != null;
|
||||||
|
}
|
||||||
|
|
||||||
public enum EverythingModeEnum {
|
public enum EverythingModeEnum {
|
||||||
/*
|
/*
|
||||||
* Don't reorder! We rely on the ordinals
|
* Don't reorder! We rely on the ordinals
|
||||||
|
@ -88,6 +88,25 @@ public interface IBundleProvider {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the results in this bundle were produced using an offset query (as opposed to a query using
|
||||||
|
* continuation pointers, page IDs, etc.) the page offset can be returned here. The server
|
||||||
|
* should then attempt to form paging links that use <code>_offset</code> instead of
|
||||||
|
* opaque page IDs.
|
||||||
|
*/
|
||||||
|
default Integer getCurrentPageOffset() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If {@link #getCurrentPageOffset()} returns a non-null value, this method must also return
|
||||||
|
* the actual page size used
|
||||||
|
*/
|
||||||
|
default Integer getCurrentPageSize() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the instant as of which this result was created. The
|
* Returns the instant as of which this result was created. The
|
||||||
* result of this value is used to populate the <code>lastUpdated</code>
|
* result of this value is used to populate the <code>lastUpdated</code>
|
||||||
@ -194,4 +213,5 @@ public interface IBundleProvider {
|
|||||||
Validate.notNull(retVal, "size() returned null");
|
Validate.notNull(retVal, "size() returned null");
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,8 @@ public class SimpleBundleProvider implements IBundleProvider {
|
|||||||
private Integer myPreferredPageSize;
|
private Integer myPreferredPageSize;
|
||||||
private Integer mySize;
|
private Integer mySize;
|
||||||
private IPrimitiveType<Date> myPublished = InstantDt.withCurrentTime();
|
private IPrimitiveType<Date> myPublished = InstantDt.withCurrentTime();
|
||||||
|
private Integer myCurrentPageOffset;
|
||||||
|
private Integer myCurrentPageSize;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
@ -74,6 +76,36 @@ public class SimpleBundleProvider implements IBundleProvider {
|
|||||||
setSize(theSize);
|
setSize(theSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 5.5.0
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Integer getCurrentPageOffset() {
|
||||||
|
return myCurrentPageOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 5.5.0
|
||||||
|
*/
|
||||||
|
public void setCurrentPageOffset(Integer theCurrentPageOffset) {
|
||||||
|
myCurrentPageOffset = theCurrentPageOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 5.5.0
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Integer getCurrentPageSize() {
|
||||||
|
return myCurrentPageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 5.5.0
|
||||||
|
*/
|
||||||
|
public void setCurrentPageSize(Integer theCurrentPageSize) {
|
||||||
|
myCurrentPageSize = theCurrentPageSize;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the results stored in this provider
|
* Returns the results stored in this provider
|
||||||
*/
|
*/
|
||||||
|
@ -47,6 +47,7 @@ import java.util.List;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
|
||||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||||
|
|
||||||
@ -141,7 +142,16 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
|
|||||||
IBaseResource createBundleFromBundleProvider(IRestfulServer<?> theServer, RequestDetails theRequest, Integer theLimit, String theLinkSelf, Set<Include> theIncludes,
|
IBaseResource createBundleFromBundleProvider(IRestfulServer<?> theServer, RequestDetails theRequest, Integer theLimit, String theLinkSelf, Set<Include> theIncludes,
|
||||||
IBundleProvider theResult, int theOffset, BundleTypeEnum theBundleType, EncodingEnum theLinkEncoding, String theSearchId) {
|
IBundleProvider theResult, int theOffset, BundleTypeEnum theBundleType, EncodingEnum theLinkEncoding, String theSearchId) {
|
||||||
IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory();
|
IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory();
|
||||||
final Integer requestOffset = RestfulServerUtils.tryToExtractNamedParameter(theRequest, Constants.PARAM_OFFSET);
|
final Integer offset;
|
||||||
|
Integer limit = theLimit;
|
||||||
|
|
||||||
|
if (theResult.getCurrentPageOffset() != null) {
|
||||||
|
offset = theResult.getCurrentPageOffset();
|
||||||
|
limit = theResult.getCurrentPageSize();
|
||||||
|
Validate.notNull(limit, "IBundleProvider returned a non-null offset, but did not return a non-null page size");
|
||||||
|
} else {
|
||||||
|
offset = RestfulServerUtils.tryToExtractNamedParameter(theRequest, Constants.PARAM_OFFSET);
|
||||||
|
}
|
||||||
|
|
||||||
int numToReturn;
|
int numToReturn;
|
||||||
String searchId = null;
|
String searchId = null;
|
||||||
@ -149,9 +159,9 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
|
|||||||
Integer numTotalResults = theResult.size();
|
Integer numTotalResults = theResult.size();
|
||||||
|
|
||||||
int pageSize;
|
int pageSize;
|
||||||
if (requestOffset != null || !theServer.canStoreSearchResults()) {
|
if (offset != null || !theServer.canStoreSearchResults()) {
|
||||||
if (theLimit != null) {
|
if (limit != null) {
|
||||||
pageSize = theLimit;
|
pageSize = limit;
|
||||||
} else {
|
} else {
|
||||||
if (theServer.getDefaultPageSize() != null) {
|
if (theServer.getDefaultPageSize() != null) {
|
||||||
pageSize = theServer.getDefaultPageSize();
|
pageSize = theServer.getDefaultPageSize();
|
||||||
@ -161,7 +171,7 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
|
|||||||
}
|
}
|
||||||
numToReturn = pageSize;
|
numToReturn = pageSize;
|
||||||
|
|
||||||
if (requestOffset != null && !isOffsetModeHistory()) {
|
if ((offset != null && !isOffsetModeHistory()) || theResult.getCurrentPageOffset() != null) {
|
||||||
// When offset query is done theResult already contains correct amount (+ their includes etc.) so return everything
|
// When offset query is done theResult already contains correct amount (+ their includes etc.) so return everything
|
||||||
resourceList = theResult.getResources(0, Integer.MAX_VALUE);
|
resourceList = theResult.getResources(0, Integer.MAX_VALUE);
|
||||||
} else if (numToReturn > 0) {
|
} else if (numToReturn > 0) {
|
||||||
@ -173,10 +183,10 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
IPagingProvider pagingProvider = theServer.getPagingProvider();
|
IPagingProvider pagingProvider = theServer.getPagingProvider();
|
||||||
if (theLimit == null || theLimit.equals(0)) {
|
if (limit == null || ((Integer) limit).equals(0)) {
|
||||||
pageSize = pagingProvider.getDefaultPageSize();
|
pageSize = pagingProvider.getDefaultPageSize();
|
||||||
} else {
|
} else {
|
||||||
pageSize = Math.min(pagingProvider.getMaximumPageSize(), theLimit);
|
pageSize = Math.min(pagingProvider.getMaximumPageSize(), limit);
|
||||||
}
|
}
|
||||||
numToReturn = pageSize;
|
numToReturn = pageSize;
|
||||||
|
|
||||||
@ -238,19 +248,31 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
|
|||||||
BundleLinks links = new BundleLinks(theRequest.getFhirServerBase(), theIncludes, RestfulServerUtils.prettyPrintResponse(theServer, theRequest), theBundleType);
|
BundleLinks links = new BundleLinks(theRequest.getFhirServerBase(), theIncludes, RestfulServerUtils.prettyPrintResponse(theServer, theRequest), theBundleType);
|
||||||
links.setSelf(theLinkSelf);
|
links.setSelf(theLinkSelf);
|
||||||
|
|
||||||
if (requestOffset != null || (!theServer.canStoreSearchResults() && !isEverythingOperation(theRequest)) || isOffsetModeHistory()) {
|
if (theResult.getCurrentPageOffset() != null) {
|
||||||
int offset = requestOffset != null ? requestOffset : 0;
|
|
||||||
|
if (isNotBlank(theResult.getNextPageId())) {
|
||||||
|
links.setNext(RestfulServerUtils.createOffsetPagingLink(links, theRequest.getRequestPath(), theRequest.getTenantId(), offset + limit, limit, theRequest.getParameters()));
|
||||||
|
}
|
||||||
|
if (isNotBlank(theResult.getPreviousPageId())) {
|
||||||
|
links.setNext(RestfulServerUtils.createOffsetPagingLink(links, theRequest.getRequestPath(), theRequest.getTenantId(), Math.max(offset - limit, 0), limit, theRequest.getParameters()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset != null || (!theServer.canStoreSearchResults() && !isEverythingOperation(theRequest)) || isOffsetModeHistory()) {
|
||||||
// Paging without caching
|
// Paging without caching
|
||||||
// We're doing requestOffset pages
|
// We're doing offset pages
|
||||||
int requestedToReturn = numToReturn;
|
int requestedToReturn = numToReturn;
|
||||||
if (theServer.getPagingProvider() == null) {
|
if (theServer.getPagingProvider() == null && offset != null) {
|
||||||
// There is no paging provider at all, so assume we're querying up to all the results we need every time
|
// There is no paging provider at all, so assume we're querying up to all the results we need every time
|
||||||
requestedToReturn += offset;
|
requestedToReturn += offset;
|
||||||
}
|
}
|
||||||
if (numTotalResults == null || requestedToReturn < numTotalResults) {
|
if (numTotalResults == null || requestedToReturn < numTotalResults) {
|
||||||
links.setNext(RestfulServerUtils.createOffsetPagingLink(links, theRequest.getRequestPath(), theRequest.getTenantId(), offset + numToReturn, numToReturn, theRequest.getParameters()));
|
if (!resourceList.isEmpty()) {
|
||||||
|
links.setNext(RestfulServerUtils.createOffsetPagingLink(links, theRequest.getRequestPath(), theRequest.getTenantId(), defaultIfNull(offset, 0) + numToReturn, numToReturn, theRequest.getParameters()));
|
||||||
}
|
}
|
||||||
if (offset > 0) {
|
}
|
||||||
|
if (offset != null && offset > 0) {
|
||||||
int start = Math.max(0, theOffset - pageSize);
|
int start = Math.max(0, theOffset - pageSize);
|
||||||
links.setPrev(RestfulServerUtils.createOffsetPagingLink(links, theRequest.getRequestPath(), theRequest.getTenantId(), start, pageSize, theRequest.getParameters()));
|
links.setPrev(RestfulServerUtils.createOffsetPagingLink(links, theRequest.getRequestPath(), theRequest.getTenantId(), start, pageSize, theRequest.getParameters()));
|
||||||
}
|
}
|
||||||
|
@ -57,88 +57,6 @@ public class SearchBundleProviderWithNoSizeDstu2Test {
|
|||||||
ourIdentifiers = 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<List<IBaseResource>>() {
|
|
||||||
@Override
|
|
||||||
public List<IBaseResource> answer(InvocationOnMock theInvocation) throws Throwable {
|
|
||||||
int from =(Integer)theInvocation.getArguments()[0];
|
|
||||||
int to =(Integer)theInvocation.getArguments()[1];
|
|
||||||
ArrayList<IBaseResource> 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());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterAll
|
@AfterAll
|
||||||
public static void afterClassClearContext() throws Exception {
|
public static void afterClassClearContext() throws Exception {
|
||||||
|
@ -57,89 +57,6 @@ public class SearchBundleProviderWithNoSizeDstu3Test {
|
|||||||
ourIdentifiers = 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<List<IBaseResource>>() {
|
|
||||||
@Override
|
|
||||||
public List<IBaseResource> answer(InvocationOnMock theInvocation) throws Throwable {
|
|
||||||
int from =(Integer)theInvocation.getArguments()[0];
|
|
||||||
int to =(Integer)theInvocation.getArguments()[1];
|
|
||||||
ArrayList<IBaseResource> 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;
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterAll
|
@AfterAll
|
||||||
public static void afterClassClearContext() throws Exception {
|
public static void afterClassClearContext() throws Exception {
|
||||||
JettyUtil.closeServer(ourServer);
|
JettyUtil.closeServer(ourServer);
|
||||||
|
@ -62,6 +62,7 @@ public class SearchBundleProviderWithNoSizeR4Test {
|
|||||||
Bundle respBundle;
|
Bundle respBundle;
|
||||||
|
|
||||||
ourLastBundleProvider = mock(IBundleProvider.class);
|
ourLastBundleProvider = mock(IBundleProvider.class);
|
||||||
|
when(ourLastBundleProvider.getCurrentPageOffset()).thenReturn(null);
|
||||||
when(ourLastBundleProvider.size()).thenReturn(null);
|
when(ourLastBundleProvider.size()).thenReturn(null);
|
||||||
when(ourLastBundleProvider.getResources(any(int.class), any(int.class))).then(new Answer<List<IBaseResource>>() {
|
when(ourLastBundleProvider.getResources(any(int.class), any(int.class))).then(new Answer<List<IBaseResource>>() {
|
||||||
@Override
|
@Override
|
||||||
|
Loading…
x
Reference in New Issue
Block a user