Add support for Cache-Control header in JPA server and client

This commit is contained in:
James 2017-10-05 13:38:53 -04:00
parent 4d1ab2734f
commit ce720f5601
20 changed files with 3211 additions and 2581 deletions

View File

@ -1,5 +1,6 @@
package example;
import ca.uhn.fhir.rest.api.CacheControlDirective;
import org.hl7.fhir.dstu3.model.Bundle;
import ca.uhn.fhir.context.FhirContext;
@ -8,6 +9,7 @@ import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.client.apache.GZipContentInterceptor;
import ca.uhn.fhir.rest.client.api.*;
import ca.uhn.fhir.rest.client.interceptor.*;
import org.hl7.fhir.r4.model.Patient;
public class ClientExamples {
@ -52,6 +54,26 @@ public class ClientExamples {
// END SNIPPET: processMessage
}
@SuppressWarnings("unused")
public void cacheControl() {
FhirContext ctx = FhirContext.forDstu3();
// Create the client
IGenericClient client = ctx.newRestfulGenericClient("http://localhost:9999/fhir");
Bundle bundle = new Bundle();
// ..populate the bundle..
// START SNIPPET: cacheControl
Bundle response = client
.search()
.forResource(Patient.class)
.returnBundle(Bundle.class)
.cacheControl(new CacheControlDirective().setNoCache(true)) // <-- add a directive
.execute();
// END SNIPPET: cacheControl
}
@SuppressWarnings("unused")
public void createOkHttp() {
// START SNIPPET: okhttp

View File

@ -0,0 +1,108 @@
package ca.uhn.fhir.rest.api;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.StringTokenizer;
import static org.apache.commons.lang3.StringUtils.trim;
/**
* Parses and stores the value(s) within HTTP Cache-Control headers
*/
public class CacheControlDirective {
private static final String MAX_RESULTS_EQUALS = Constants.CACHE_CONTROL_MAX_RESULTS + "=";
private static final Logger ourLog = LoggerFactory.getLogger(CacheControlDirective.class);
private boolean myNoCache;
private boolean myNoStore;
private Integer myMaxResults;
/**
* Constructor
*/
public CacheControlDirective() {
super();
}
/**
* If the {@link #isNoStore() no-store} directive is set, this HAPI FHIR extention
* to the <code>Cache-Control</code> header called <code>max-results=123</code>
* specified the maximum number of results which will be fetched from the
* database before returning.
*/
public Integer getMaxResults() {
return myMaxResults;
}
/**
* If the {@link #isNoStore() no-store} directive is set, this HAPI FHIR extention
* to the <code>Cache-Control</code> header called <code>max-results=123</code>
* specified the maximum number of results which will be fetched from the
* database before returning.
*/
public CacheControlDirective setMaxResults(Integer theMaxResults) {
myMaxResults = theMaxResults;
return this;
}
/**
* If <code>true<</code>, adds the <code>no-cache</code> directive to the
* request. This directive indicates that the cache should not be used to
* serve this request.
*/
public boolean isNoCache() {
return myNoCache;
}
/**
* If <code>true<</code>, adds the <code>no-cache</code> directive to the
* request. This directive indicates that the cache should not be used to
* serve this request.
*/
public CacheControlDirective setNoCache(boolean theNoCache) {
myNoCache = theNoCache;
return this;
}
public boolean isNoStore() {
return myNoStore;
}
public CacheControlDirective setNoStore(boolean theNoStore) {
myNoStore = theNoStore;
return this;
}
/**
* Parses a list of <code>Cache-Control</code> header values
*
* @param theValues The <code>Cache-Control</code> header values
*/
public CacheControlDirective parse(List<String> theValues) {
if (theValues != null) {
for (String nextValue : theValues) {
StringTokenizer tok = new StringTokenizer(nextValue, ",");
while (tok.hasMoreTokens()) {
String next = trim(tok.nextToken());
if (Constants.CACHE_CONTROL_NO_CACHE.equals(next)) {
myNoCache = true;
} else if (Constants.CACHE_CONTROL_NO_STORE.equals(next)) {
myNoStore = true;
} else if (next.startsWith(MAX_RESULTS_EQUALS)) {
String valueString = trim(next.substring(MAX_RESULTS_EQUALS.length()));
try {
myMaxResults = Integer.parseInt(valueString);
} catch (NumberFormatException e) {
ourLog.warn("Invalid {} value: {}", Constants.CACHE_CONTROL_MAX_RESULTS, valueString);
}
}
}
}
}
return this;
}
}

View File

@ -25,6 +25,9 @@ import java.util.*;
public class Constants {
public static final String CACHE_CONTROL_MAX_RESULTS = "max-results";
public static final String CACHE_CONTROL_NO_CACHE = "no-cache";
public static final String CACHE_CONTROL_NO_STORE = "no-store";
public static final String CHARSET_NAME_UTF8 = "UTF-8";
public static final Charset CHARSET_UTF8;
public static final String CHARSET_UTF8_CTSUFFIX = "; charset=" + CHARSET_NAME_UTF8;
@ -67,6 +70,7 @@ public class Constants {
public static final String HEADER_AUTHORIZATION = "Authorization";
public static final String HEADER_AUTHORIZATION_VALPREFIX_BASIC = "Basic ";
public static final String HEADER_AUTHORIZATION_VALPREFIX_BEARER = "Bearer ";
public static final String HEADER_CACHE_CONTROL = "Cache-Control";
public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
public static final String HEADER_CONTENT_ENCODING = "Content-Encoding";
public static final String HEADER_CONTENT_LOCATION = "Content-Location";

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.rest.gclient;
import ca.uhn.fhir.rest.api.CacheControlDirective;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.SummaryEnum;
import org.hl7.fhir.instance.model.api.IBaseResource;
@ -38,6 +39,12 @@ public interface IClientExecutable<T extends IClientExecutable<?,Y>, Y> {
@Deprecated
T andLogRequestAndResponse(boolean theLogRequestAndResponse);
/**
* Sets the <code>Cache-Control</code> header value, which advises the server (or any cache in front of it)
* how to behave in terms of cached requests
*/
T cacheControl(CacheControlDirective theCacheControlDirective);
/**
* Request that the server return subsetted resources, containing only the elements specified in the given parameters.
* For example: <code>subsetElements("name", "identifier")</code> requests that the server only return

View File

@ -0,0 +1,58 @@
package ca.uhn.fhir.rest.api;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.*;
public class CacheControlDirectiveTest {
@Test
public void testParseNoCache() {
List<String> values = Arrays.asList(Constants.CACHE_CONTROL_NO_CACHE);
CacheControlDirective ccd = new CacheControlDirective();
ccd.parse(values);
assertTrue(ccd.isNoCache());
assertFalse(ccd.isNoStore());
}
@Test
public void testParseNoCacheNoStore() {
List<String> values = Arrays.asList(Constants.CACHE_CONTROL_NO_CACHE + " , " + Constants.CACHE_CONTROL_NO_STORE);
CacheControlDirective ccd = new CacheControlDirective();
ccd.parse(values);
assertTrue(ccd.isNoCache());
assertTrue(ccd.isNoStore());
assertEquals(null, ccd.getMaxResults());
}
@Test
public void testParseNoCacheNoStoreMaxResults() {
List<String> values = Arrays.asList(Constants.CACHE_CONTROL_NO_STORE + ", "+ Constants.CACHE_CONTROL_MAX_RESULTS + "=5");
CacheControlDirective ccd = new CacheControlDirective();
ccd.parse(values);
assertFalse(ccd.isNoCache());
assertTrue(ccd.isNoStore());
assertEquals(5, ccd.getMaxResults().intValue());
}
@Test
public void testParseNoCacheNoStoreMaxResultsInvalid() {
List<String> values = Arrays.asList(Constants.CACHE_CONTROL_NO_STORE + ", "+ Constants.CACHE_CONTROL_MAX_RESULTS + "=A");
CacheControlDirective ccd = new CacheControlDirective();
ccd.parse(values);
assertFalse(ccd.isNoCache());
assertTrue(ccd.isNoStore());
assertEquals(null, ccd.getMaxResults());
}
@Test
public void testParseNull() {
CacheControlDirective ccd = new CacheControlDirective();
ccd.parse(null);
assertFalse(ccd.isNoCache());
assertFalse(ccd.isNoStore());
}
}

View File

@ -34,6 +34,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import ca.uhn.fhir.rest.api.CacheControlDirective;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
@ -135,7 +136,7 @@ public abstract class BaseClient implements IRestfulClient {
public <T extends IBaseResource> T fetchResourceFromUrl(Class<T> theResourceType, String theUrl) {
BaseHttpClientInvocation clientInvocation = new HttpGetClientInvocation(getFhirContext(), theUrl);
ResourceResponseHandler<T> binding = new ResourceResponseHandler<T>(theResourceType);
return invokeClient(getFhirContext(), binding, clientInvocation, null, false, false, null, null);
return invokeClient(getFhirContext(), binding, clientInvocation, null, false, false, null, null, null);
}
void forceConformanceCheck() {
@ -198,11 +199,11 @@ public abstract class BaseClient implements IRestfulClient {
}
<T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation, boolean theLogRequestAndResponse) {
return invokeClient(theContext, binding, clientInvocation, null, null, theLogRequestAndResponse, null, null);
return invokeClient(theContext, binding, clientInvocation, null, null, theLogRequestAndResponse, null, null, null);
}
<T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation, EncodingEnum theEncoding, Boolean thePrettyPrint,
boolean theLogRequestAndResponse, SummaryEnum theSummaryMode, Set<String> theSubsetElements) {
boolean theLogRequestAndResponse, SummaryEnum theSummaryMode, Set<String> theSubsetElements, CacheControlDirective theCacheControlDirective) {
if (!myDontValidateConformance) {
myFactory.validateServerBaseIfConfiguredToDoSo(myUrlBase, myClient, this);
@ -244,6 +245,18 @@ public abstract class BaseClient implements IRestfulClient {
httpRequest = clientInvocation.asHttpRequest(myUrlBase, params, encoding, thePrettyPrint);
if (theCacheControlDirective != null) {
StringBuilder b = new StringBuilder();
addToCacheControlHeader(b, Constants.CACHE_CONTROL_NO_CACHE, theCacheControlDirective.isNoCache());
addToCacheControlHeader(b, Constants.CACHE_CONTROL_NO_STORE, theCacheControlDirective.isNoStore());
if (theCacheControlDirective.getMaxResults() != null) {
addToCacheControlHeader(b, Constants.CACHE_CONTROL_MAX_RESULTS+"="+ Integer.toString(theCacheControlDirective.getMaxResults().intValue()), true);
}
if (b.length() > 0) {
httpRequest.addHeader(Constants.HEADER_CACHE_CONTROL, b.toString());
}
}
if (theLogRequestAndResponse) {
ourLog.info("Client invoking: {}", httpRequest);
String body = httpRequest.getRequestBodyFromStream();
@ -366,6 +379,15 @@ public abstract class BaseClient implements IRestfulClient {
}
}
private void addToCacheControlHeader(StringBuilder theBuilder, String theDirective, boolean theActive) {
if (theActive) {
if (theBuilder.length() > 0) {
theBuilder.append(", ");
}
theBuilder.append(theDirective);
}
}
/**
* For now, this is a part of the internal API of HAPI - Use with caution as this method may change!
*/

View File

@ -120,10 +120,10 @@ public class GenericClient extends BaseClient implements IGenericClient {
ResourceResponseHandler<T> binding = new ResourceResponseHandler<T>(theType, (Class<? extends IBaseResource>) null, id, allowHtmlResponse);
if (theNotModifiedHandler == null) {
return invokeClient(myContext, binding, invocation, theEncoding, thePrettyPrint, myLogRequestAndResponse, theSummary, theSubsetElements);
return invokeClient(myContext, binding, invocation, theEncoding, thePrettyPrint, myLogRequestAndResponse, theSummary, theSubsetElements, null);
}
try {
return invokeClient(myContext, binding, invocation, theEncoding, thePrettyPrint, myLogRequestAndResponse, theSummary, theSubsetElements);
return invokeClient(myContext, binding, invocation, theEncoding, thePrettyPrint, myLogRequestAndResponse, theSummary, theSubsetElements, null);
} catch (NotModifiedException e) {
return theNotModifiedHandler.call();
}
@ -373,6 +373,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
private boolean myQueryLogRequestAndResponse;
private HashSet<String> mySubsetElements;
protected SummaryEnum mySummaryMode;
protected CacheControlDirective myCacheControlDirective;
@Deprecated // override deprecated method
@SuppressWarnings("unchecked")
@ -382,6 +383,12 @@ public class GenericClient extends BaseClient implements IGenericClient {
return (T) this;
}
@Override
public T cacheControl(CacheControlDirective theCacheControlDirective) {
myCacheControlDirective = theCacheControlDirective;
return (T) this;
}
@SuppressWarnings("unchecked")
@Override
public T elementsSubset(String... theElements) {
@ -434,19 +441,11 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
protected <Z> Z invoke(Map<String, List<String>> theParams, IClientResponseHandler<Z> theHandler, BaseHttpClientInvocation theInvocation) {
// if (myParamEncoding != null) {
// theParams.put(Constants.PARAM_FORMAT, Collections.singletonList(myParamEncoding.getFormatContentType()));
// }
//
// if (myPrettyPrint != null) {
// theParams.put(Constants.PARAM_PRETTY, Collections.singletonList(myPrettyPrint.toString()));
// }
if (isKeepResponses()) {
myLastRequest = theInvocation.asHttpRequest(getServerBase(), theParams, getEncoding(), myPrettyPrint);
}
Z resp = invokeClient(myContext, theHandler, theInvocation, myParamEncoding, myPrettyPrint, myQueryLogRequestAndResponse || myLogRequestAndResponse, mySummaryMode, mySubsetElements);
Z resp = invokeClient(myContext, theHandler, theInvocation, myParamEncoding, myPrettyPrint, myQueryLogRequestAndResponse || myLogRequestAndResponse, mySummaryMode, mySubsetElements, myCacheControlDirective);
return resp;
}

View File

@ -36,10 +36,7 @@ import ca.uhn.fhir.jpa.util.jsonpatch.JsonPatchUtils;
import ca.uhn.fhir.jpa.util.xmlpatch.XmlPatchUtils;
import ca.uhn.fhir.model.api.*;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.PatchTypeEnum;
import ca.uhn.fhir.rest.api.QualifiedParamList;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.ParameterUtil;
@ -928,7 +925,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
}
return mySearchCoordinatorSvc.registerSearch(this, theParams, getResourceName());
CacheControlDirective cacheControlDirective = new CacheControlDirective();
if (theRequestDetails != null) {
cacheControlDirective.parse(theRequestDetails.getHeaders(Constants.HEADER_CACHE_CONTROL));
}
return mySearchCoordinatorSvc.registerSearch(this, theParams, getResourceName(), cacheControlDirective);
}
@Override

View File

@ -107,6 +107,7 @@ public class DaoConfig {
private Set<String> myTreatBaseUrlsAsLocal = new HashSet<String>();
private Set<String> myTreatReferencesAsLogical = new HashSet<String>(DEFAULT_LOGICAL_BASE_URLS);
private boolean myAutoCreatePlaceholderReferenceTargets;
private Integer myCacheControlNoStoreMaxResultsUpperLimit = 1000;
/**
* Constructor
@ -131,6 +132,26 @@ public class DaoConfig {
myTreatReferencesAsLogical.add(theTreatReferencesAsLogical);
}
/**
* Specifies the highest number that a client is permitted to use in a
* <code>Cache-Control: nostore, max-results=NNN</code>
* directive. If the client tries to exceed this limit, the
* request will be denied. Defaults to 1000.
*/
public Integer getCacheControlNoStoreMaxResultsUpperLimit() {
return myCacheControlNoStoreMaxResultsUpperLimit;
}
/**
* Specifies the highest number that a client is permitted to use in a
* <code>Cache-Control: nostore, max-results=NNN</code>
* directive. If the client tries to exceed this limit, the
* request will be denied. Defaults to 1000.
*/
public void setCacheControlNoStoreMaxResultsUpperLimit(Integer theCacheControlNoStoreMaxResults) {
myCacheControlNoStoreMaxResultsUpperLimit = theCacheControlNoStoreMaxResults;
}
/**
* When a code system is added that contains more than this number of codes,
* the code system will be indexed later in an incremental process in order to
@ -336,8 +357,11 @@ public class DaoConfig {
/**
* This may be used to optionally register server interceptors directly against the DAOs.
*/
public void setInterceptors(List<IServerInterceptor> theInterceptors) {
myInterceptors = theInterceptors;
public void setInterceptors(IServerInterceptor... theInterceptor) {
setInterceptors(new ArrayList<IServerInterceptor>());
if (theInterceptor != null && theInterceptor.length != 0) {
getInterceptors().addAll(Arrays.asList(theInterceptor));
}
}
/**
@ -434,6 +458,11 @@ public class DaoConfig {
* This approach can improve performance, especially under heavy load, but can also mean that
* searches may potentially return slightly out-of-date results.
* </p>
* <p>
* Note that if this is set to a non-null value, clients may override this setting by using
* the <code>Cache-Control</code> header. If this is set to <code>null</code>, the Cache-Control
* header will be ignored.
* </p>
*/
public Long getReuseCachedSearchResultsForMillis() {
return myReuseCachedSearchResultsForMillis;
@ -449,6 +478,11 @@ public class DaoConfig {
* This approach can improve performance, especially under heavy load, but can also mean that
* searches may potentially return slightly out-of-date results.
* </p>
* <p>
* Note that if this is set to a non-null value, clients may override this setting by using
* the <code>Cache-Control</code> header. If this is set to <code>null</code>, the Cache-Control
* header will be ignored.
* </p>
*/
public void setReuseCachedSearchResultsForMillis(Long theReuseCachedSearchResultsForMillis) {
myReuseCachedSearchResultsForMillis = theReuseCachedSearchResultsForMillis;
@ -925,11 +959,8 @@ public class DaoConfig {
/**
* This may be used to optionally register server interceptors directly against the DAOs.
*/
public void setInterceptors(IServerInterceptor... theInterceptor) {
setInterceptors(new ArrayList<IServerInterceptor>());
if (theInterceptor != null && theInterceptor.length != 0) {
getInterceptors().addAll(Arrays.asList(theInterceptor));
}
public void setInterceptors(List<IServerInterceptor> theInterceptors) {
myInterceptors = theInterceptors;
}
/**

View File

@ -65,7 +65,7 @@ public class FhirResourceDaoPatientDstu2 extends FhirResourceDaoDstu2<Patient>im
paramMap.setLoadSynchronous(true);
}
return mySearchCoordinatorSvc.registerSearch(this, paramMap, getResourceName());
return mySearchCoordinatorSvc.registerSearch(this, paramMap, getResourceName(), new CacheControlDirective().parse(theRequestDetails.getHeaders(Constants.HEADER_CACHE_CONTROL)));
}
@Override

View File

@ -24,6 +24,7 @@ import java.util.Collections;
import javax.servlet.http.HttpServletRequest;
import ca.uhn.fhir.rest.api.CacheControlDirective;
import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
@ -66,7 +67,7 @@ public class FhirResourceDaoPatientDstu3 extends FhirResourceDaoDstu3<Patient>im
paramMap.setLoadSynchronous(true);
}
return mySearchCoordinatorSvc.registerSearch(this, paramMap, getResourceName());
return mySearchCoordinatorSvc.registerSearch(this, paramMap, getResourceName(), new CacheControlDirective().parse(theRequestDetails.getHeaders(Constants.HEADER_CACHE_CONTROL)));
}
@Override

View File

@ -24,6 +24,7 @@ import java.util.Collections;
import javax.servlet.http.HttpServletRequest;
import ca.uhn.fhir.rest.api.CacheControlDirective;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
@ -66,7 +67,7 @@ public class FhirResourceDaoPatientR4 extends FhirResourceDaoR4<Patient>implemen
paramMap.setLoadSynchronous(true);
}
return mySearchCoordinatorSvc.registerSearch(this, paramMap, getResourceName());
return mySearchCoordinatorSvc.registerSearch(this, paramMap, getResourceName(), new CacheControlDirective().parse(theRequestDetails.getHeaders(Constants.HEADER_CACHE_CONTROL)));
}
@Override

View File

@ -20,18 +20,19 @@ package ca.uhn.fhir.jpa.search;
* #L%
*/
import java.util.List;
import ca.uhn.fhir.jpa.dao.IDao;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.rest.api.CacheControlDirective;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import java.util.List;
public interface ISearchCoordinatorSvc {
List<Long> getResources(String theUuid, int theFrom, int theTo);
IBundleProvider registerSearch(IDao theCallingDao, SearchParameterMap theParams, String theResourceType);
void cancelAllActiveSearches();
List<Long> getResources(String theUuid, int theFrom, int theTo);
IBundleProvider registerSearch(IDao theCallingDao, SearchParameterMap theParams, String theResourceType, CacheControlDirective theCacheControlDirective);
}

View File

@ -24,6 +24,8 @@ import java.util.concurrent.*;
import javax.persistence.EntityManager;
import ca.uhn.fhir.rest.api.CacheControlDirective;
import ca.uhn.fhir.rest.api.Constants;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.exception.ExceptionUtils;
@ -55,7 +57,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
public static final int DEFAULT_SYNC_SIZE = 250;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchCoordinatorSvcImpl.class);
private final ConcurrentHashMap<String, SearchTask> myIdToSearchTask = new ConcurrentHashMap<String, SearchTask>();
@Autowired
private FhirContext myContext;
@Autowired
@ -63,7 +65,6 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
@Autowired
private EntityManager myEntityManager;
private ExecutorService myExecutor;
private final ConcurrentHashMap<String, SearchTask> myIdToSearchTask = new ConcurrentHashMap<String, SearchTask>();
private Integer myLoadingThrottleForUnitTests = null;
private long myMaxMillisToWaitForRemoteResults = DateUtils.MILLIS_PER_MINUTE;
private boolean myNeverUseLocalSearchForUnitTests;
@ -186,7 +187,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
}
@Override
public IBundleProvider registerSearch(final IDao theCallingDao, final SearchParameterMap theParams, String theResourceType) {
public IBundleProvider registerSearch(final IDao theCallingDao, final SearchParameterMap theParams, String theResourceType, CacheControlDirective theCacheControlDirective) {
StopWatch w = new StopWatch();
final String searchUuid = UUID.randomUUID().toString();
@ -194,7 +195,21 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
final ISearchBuilder sb = theCallingDao.newSearchBuilder();
sb.setType(resourceTypeClass, theResourceType);
if (theParams.isLoadSynchronous()) {
final Integer loadSynchronousUpTo;
if (theCacheControlDirective != null && theCacheControlDirective.isNoStore()) {
if (theCacheControlDirective.getMaxResults() != null) {
loadSynchronousUpTo = theCacheControlDirective.getMaxResults();
if (loadSynchronousUpTo > myDaoConfig.getCacheControlNoStoreMaxResultsUpperLimit()) {
throw new InvalidRequestException(Constants.HEADER_CACHE_CONTROL + " header " + Constants.CACHE_CONTROL_MAX_RESULTS + " value must not exceed " + myDaoConfig.getCacheControlNoStoreMaxResultsUpperLimit());
}
} else {
loadSynchronousUpTo = 100;
}
} else {
loadSynchronousUpTo = null;
}
if (theParams.isLoadSynchronous() || loadSynchronousUpTo != null) {
// Execute the query and make sure we return distinct results
TransactionTemplate txTemplate = new TransactionTemplate(myManagedTxManager);
@ -209,6 +224,9 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
Iterator<Long> resultIter = sb.createQuery(theParams, searchUuid);
while (resultIter.hasNext()) {
pids.add(resultIter.next());
if (loadSynchronousUpTo != null && pids.size() >= loadSynchronousUpTo) {
break;
}
if (theParams.getLoadSynchronousUpTo() != null && pids.size() >= theParams.getLoadSynchronousUpTo()) {
break;
}
@ -238,9 +256,13 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
* See if there are any cached searches whose results we can return
* instead
*/
boolean useCache = true;
if (theCacheControlDirective != null && theCacheControlDirective.isNoCache() == true) {
useCache = false;
}
final String queryString = theParams.toNormalizedQueryString(myContext);
if (theParams.getEverythingMode() == null) {
if (myDaoConfig.getReuseCachedSearchResultsForMillis() != null) {
if (myDaoConfig.getReuseCachedSearchResultsForMillis() != null && useCache) {
final Date createdCutoff = new Date(System.currentTimeMillis() - myDaoConfig.getReuseCachedSearchResultsForMillis());
final String resourceType = theResourceType;
@ -401,16 +423,16 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
public class SearchTask implements Callable<Void> {
private boolean myAbortRequested;
private final IDao myCallingDao;
private final CountDownLatch myCompletionLatch;
private int myCountSaved = 0;
private final CountDownLatch myInitialCollectionLatch = new CountDownLatch(1);
private final SearchParameterMap myParams;
private final String myResourceType;
private final Search mySearch;
private final ArrayList<Long> mySyncedPids = new ArrayList<Long>();
private final ArrayList<Long> myUnsyncedPids = new ArrayList<Long>();
private boolean myAbortRequested;
private int myCountSaved = 0;
private String mySearchUuid;
public SearchTask(Search theSearch, IDao theCallingDao, SearchParameterMap theParams, String theResourceType, String theSearchUuid) {

View File

@ -0,0 +1,164 @@
package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
import ca.uhn.fhir.parser.StrictErrorHandler;
import ca.uhn.fhir.rest.api.CacheControlDirective;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.TestUtil;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Patient;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Test;
import org.springframework.test.util.AopTestUtils;
import java.io.IOException;
import static org.junit.Assert.*;
public class ResourceProviderR4CacheTest extends BaseResourceProviderR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceProviderR4CacheTest.class);
private SearchCoordinatorSvcImpl mySearchCoordinatorSvcRaw;
@Override
@After
public void after() throws Exception {
super.after();
myDaoConfig.setReuseCachedSearchResultsForMillis(new DaoConfig().getReuseCachedSearchResultsForMillis());
myDaoConfig.setCacheControlNoStoreMaxResultsUpperLimit(new DaoConfig().getCacheControlNoStoreMaxResultsUpperLimit());
}
@Override
public void before() throws Exception {
super.before();
myFhirCtx.setParserErrorHandler(new StrictErrorHandler());
mySearchCoordinatorSvcRaw = AopTestUtils.getTargetObject(mySearchCoordinatorSvc);
}
@Test
public void testCacheNoStore() throws IOException {
Patient pt1 = new Patient();
pt1.addName().setFamily("FAM");
ourClient.create().resource(pt1).execute();
Bundle results = ourClient
.search()
.forResource("Patient")
.where(Patient.FAMILY.matches().value("FAM"))
.returnBundle(Bundle.class)
.cacheControl(new CacheControlDirective().setNoStore(true))
.execute();
assertEquals(1, results.getEntry().size());
assertEquals(0, mySearchEntityDao.count());
Patient pt2 = new Patient();
pt2.addName().setFamily("FAM");
ourClient.create().resource(pt2).execute();
results = ourClient
.search()
.forResource("Patient")
.where(Patient.FAMILY.matches().value("FAM"))
.returnBundle(Bundle.class)
.cacheControl(new CacheControlDirective().setNoStore(true))
.execute();
assertEquals(2, results.getEntry().size());
assertEquals(0, mySearchEntityDao.count());
}
@Test
public void testCacheNoStoreMaxResults() throws IOException {
for (int i = 0; i < 10; i++) {
Patient pt1 = new Patient();
pt1.addName().setFamily("FAM" + i);
ourClient.create().resource(pt1).execute();
}
Bundle results = ourClient
.search()
.forResource("Patient")
.where(Patient.FAMILY.matches().value("FAM"))
.returnBundle(Bundle.class)
.cacheControl(new CacheControlDirective().setNoStore(true).setMaxResults(5))
.execute();
assertEquals(5, results.getEntry().size());
assertEquals(0, mySearchEntityDao.count());
}
@Test
public void testCacheNoStoreMaxResultsWithIllegalValue() throws IOException {
myDaoConfig.setCacheControlNoStoreMaxResultsUpperLimit(123);
try {
ourClient
.search()
.forResource("Patient")
.where(Patient.FAMILY.matches().value("FAM"))
.returnBundle(Bundle.class)
.cacheControl(new CacheControlDirective().setNoStore(true).setMaxResults(5000))
.execute();
fail();
} catch (InvalidRequestException e) {
assertEquals("HTTP 400 Bad Request: Cache-Control header max-results value must not exceed 123", e.getMessage());
}
}
@Test
public void testCacheSuppressed() throws IOException {
Patient pt1 = new Patient();
pt1.addName().setFamily("FAM");
ourClient.create().resource(pt1).execute();
Bundle results = ourClient.search().forResource("Patient").where(Patient.FAMILY.matches().value("FAM")).returnBundle(Bundle.class).execute();
assertEquals(1, results.getEntry().size());
assertEquals(1, mySearchEntityDao.count());
Patient pt2 = new Patient();
pt2.addName().setFamily("FAM");
ourClient.create().resource(pt2).execute();
results = ourClient
.search()
.forResource("Patient")
.where(Patient.FAMILY.matches().value("FAM"))
.returnBundle(Bundle.class)
.cacheControl(new CacheControlDirective().setNoCache(true))
.execute();
assertEquals(2, results.getEntry().size());
assertEquals(2, mySearchEntityDao.count());
}
@Test
public void testCacheUsedNormally() throws IOException {
Patient pt1 = new Patient();
pt1.addName().setFamily("FAM");
ourClient.create().resource(pt1).execute();
Bundle results = ourClient.search().forResource("Patient").where(Patient.FAMILY.matches().value("FAM")).returnBundle(Bundle.class).execute();
assertEquals(1, results.getEntry().size());
assertEquals(1, mySearchEntityDao.count());
Patient pt2 = new Patient();
pt2.addName().setFamily("FAM");
ourClient.create().resource(pt2).execute();
results = ourClient.search().forResource("Patient").where(Patient.FAMILY.matches().value("FAM")).returnBundle(Bundle.class).execute();
assertEquals(1, results.getEntry().size());
assertEquals(1, mySearchEntityDao.count());
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
}

View File

@ -1,41 +1,60 @@
package ca.uhn.fhir.jpa.search;
import static org.junit.Assert.*;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import java.util.*;
import javax.persistence.EntityManager;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.*;
import org.junit.runner.RunWith;
import org.mockito.*;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.data.domain.*;
import org.springframework.transaction.PlatformTransactionManager;
import com.google.common.collect.Lists;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.dao.data.*;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.IDao;
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.jpa.dao.data.ISearchDao;
import ca.uhn.fhir.jpa.dao.data.ISearchIncludeDao;
import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.entity.SearchResult;
import ca.uhn.fhir.jpa.entity.SearchStatusEnum;
import ca.uhn.fhir.jpa.entity.SearchTypeEnum;
import ca.uhn.fhir.jpa.util.BaseIterator;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.rest.api.CacheControlDirective;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.TestUtil;
import com.google.common.collect.Lists;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.transaction.PlatformTransactionManager;
import javax.persistence.EntityManager;
import java.util.*;
import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.same;
import static org.mockito.Mockito.*;
@SuppressWarnings({"unchecked"})
@RunWith(MockitoJUnitRunner.class)
public class SearchCoordinatorSvcImplTest {
private static FhirContext ourCtx = FhirContext.forDstu3();
@Captor
ArgumentCaptor<Iterable<SearchResult>> mySearchResultIterCaptor;
@Mock
private IDao myCallingDao;
@Mock
@ -49,10 +68,6 @@ public class SearchCoordinatorSvcImplTest {
private ISearchIncludeDao mySearchIncludeDao;
@Mock
private ISearchResultDao mySearchResultDao;
@Captor
ArgumentCaptor<Iterable<SearchResult>> mySearchResultIterCaptor;
private SearchCoordinatorSvcImpl mySvc;
@Mock
@ -63,6 +78,7 @@ public class SearchCoordinatorSvcImplTest {
public void after() {
verify(myCallingDao, atMost(myExpectedNumberOfSearchBuildersCreated)).newSearchBuilder();
}
@Before
public void before() {
@ -89,7 +105,8 @@ public class SearchCoordinatorSvcImplTest {
provider.setEntityManager(myEntityManager);
provider.setContext(ourCtx);
return null;
}}).when(myCallingDao).injectDependenciesIntoBundleProvider(any(PersistedJpaBundleProvider.class));
}
}).when(myCallingDao).injectDependenciesIntoBundleProvider(any(PersistedJpaBundleProvider.class));
}
private List<Long> createPidSequence(int from, int to) {
@ -128,7 +145,7 @@ public class SearchCoordinatorSvcImplTest {
doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(List.class), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao));
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient");
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective());
assertNotNull(result.getUuid());
assertEquals(null, result.size());
@ -151,7 +168,7 @@ public class SearchCoordinatorSvcImplTest {
doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(List.class), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao));
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient");
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective());
assertNotNull(result.getUuid());
assertEquals(null, result.size());
@ -187,7 +204,7 @@ public class SearchCoordinatorSvcImplTest {
doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(List.class), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao));
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient");
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective());
assertNotNull(result.getUuid());
assertEquals(null, result.size());
@ -215,7 +232,7 @@ public class SearchCoordinatorSvcImplTest {
doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(List.class), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao));
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient");
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective());
assertNotNull(result.getUuid());
assertEquals(null, result.size());
@ -265,7 +282,7 @@ public class SearchCoordinatorSvcImplTest {
doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(List.class), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao));
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient");
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective());
assertNotNull(result.getUuid());
assertEquals(90, result.size().intValue());
@ -318,7 +335,8 @@ public class SearchCoordinatorSvcImplTest {
}
return new PageImpl<SearchResult>(results);
}});
}
});
search.setStatus(SearchStatusEnum.FINISHED);
}
}.start();
@ -353,7 +371,7 @@ public class SearchCoordinatorSvcImplTest {
doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(eq(pids), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao));
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient");
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective());
assertNull(result.getUuid());
assertEquals(790, result.size().intValue());
@ -375,7 +393,7 @@ public class SearchCoordinatorSvcImplTest {
pids = createPidSequence(10, 110);
doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(eq(pids), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao));
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient");
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective());
assertNull(result.getUuid());
assertEquals(100, result.size().intValue());

View File

@ -53,8 +53,8 @@ import ca.uhn.fhir.util.*;
public class GenericClientTest {
private static FhirContext ourCtx;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(GenericClientTest.class);
private static FhirContext ourCtx;
private HttpClient myHttpClient;
private HttpResponse myHttpResponse;
@ -71,30 +71,40 @@ public class GenericClientTest {
System.setProperty(BaseClient.HAPI_CLIENT_KEEPRESPONSES, "true");
}
private Patient createPatientP1() {
Patient p1 = new Patient();
p1.addIdentifier().setSystem("foo:bar").setValue("12345");
p1.addName().setFamily("Smith").addGiven("John");
return p1;
}
private Bundle createTransactionBundleInput() {
Bundle input = new Bundle();
input.setType(BundleType.TRANSACTION);
input
.addEntry()
.setResource(createPatientP1())
.getRequest()
.setMethod(HTTPVerb.POST);
return input;
}
private Bundle createTransactionBundleOutput() {
Bundle output = new Bundle();
output.setType(BundleType.TRANSACTIONRESPONSE);
output
.addEntry()
.setResource(createPatientP1())
.getResponse()
.setLocation(createPatientP1().getId());
return output;
}
private String extractBody(ArgumentCaptor<HttpUriRequest> capt, int count) throws IOException {
String body = IOUtils.toString(((HttpEntityEnclosingRequestBase) capt.getAllValues().get(count)).getEntity().getContent(), "UTF-8");
return body;
}
@Test
@Ignore
public void testInvalidCalls() {
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
try {
client.meta();
fail();
} catch (IllegalStateException e) {
assertEquals("Can not call $meta operations on a DSTU1 client", e.getMessage());
}
try {
client.operation();
fail();
} catch (IllegalStateException e) {
assertEquals("Operations are only supported in FHIR DSTU2 and later. This client was created using a context configured for DSTU1", e.getMessage());
}
}
private String getPatientFeedWithOneResult() {
return ClientR4Test.getPatientFeedWithOneResult(ourCtx);
// //@formatter:off
@ -139,6 +149,81 @@ public class GenericClientTest {
return msg;
}
@SuppressWarnings("unused")
@Test
public void testCacheControlNoStore() throws Exception {
String msg = ourCtx.newXmlParser().encodeResourceToString(new Bundle());
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")));
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
Bundle response = client.search()
.forResource(Observation.class)
.returnBundle(Bundle.class)
.cacheControl(new CacheControlDirective().setNoStore(true))
.execute();
assertEquals("http://example.com/fhir/Observation", capt.getValue().getURI().toString());
assertEquals(1, capt.getValue().getHeaders("Cache-Control").length);
assertEquals("no-store", capt.getValue().getHeaders("Cache-Control")[0].getValue());
}
@SuppressWarnings("unused")
@Test
public void testCacheControlNoStoreMaxResults() throws Exception {
String msg = ourCtx.newXmlParser().encodeResourceToString(new Bundle());
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")));
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
Bundle response = client.search()
.forResource(Observation.class)
.returnBundle(Bundle.class)
.cacheControl(new CacheControlDirective().setNoStore(true).setMaxResults(100))
.execute();
assertEquals("http://example.com/fhir/Observation", capt.getValue().getURI().toString());
assertEquals(1, capt.getValue().getHeaders("Cache-Control").length);
assertEquals("no-store, max-results=100", capt.getValue().getHeaders("Cache-Control")[0].getValue());
}
@SuppressWarnings("unused")
@Test
public void testCacheControlNoStoreNoCache() throws Exception {
String msg = ourCtx.newXmlParser().encodeResourceToString(new Bundle());
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")));
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
Bundle response = client.search()
.forResource(Observation.class)
.returnBundle(Bundle.class)
.cacheControl(new CacheControlDirective().setNoStore(true).setNoCache(true))
.execute();
assertEquals("http://example.com/fhir/Observation", capt.getValue().getURI().toString());
assertEquals(1, capt.getValue().getHeaders("Cache-Control").length);
assertEquals("no-cache, no-store", capt.getValue().getHeaders("Cache-Control")[0].getValue());
}
@Test
public void testCreatePopulatesIsCreated() throws Exception {
@ -165,13 +250,6 @@ public class GenericClientTest {
ourLog.info("lastResponseBody: {}", ((GenericClient) client).getLastResponseBody());
}
private Patient createPatientP1() {
Patient p1 = new Patient();
p1.addIdentifier().setSystem("foo:bar").setValue("12345");
p1.addName().setFamily("Smith").addGiven("John");
return p1;
}
@Test
public void testCreateWithStringAutoDetectsEncoding() throws Exception {
@ -401,6 +479,48 @@ public class GenericClientTest {
idx++;
}
@Test
@Ignore
public void testInvalidCalls() {
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
try {
client.meta();
fail();
} catch (IllegalStateException e) {
assertEquals("Can not call $meta operations on a DSTU1 client", e.getMessage());
}
try {
client.operation();
fail();
} catch (IllegalStateException e) {
assertEquals("Operations are only supported in FHIR DSTU2 and later. This client was created using a context configured for DSTU1", e.getMessage());
}
}
@Test
public void testLoadPageAndReturnDstu1Bundle() throws Exception {
String msg = getPatientFeedWithOneResult();
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")));
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
IGenericClient client = ourCtx.newRestfulGenericClient("http://foo");
client
.loadPage()
.byUrl("http://example.com/page1")
.andReturnBundle(Bundle.class)
.execute();
assertEquals("http://example.com/page1", capt.getValue().getURI().toString());
}
@Test
public void testMissing() throws Exception {
@ -597,29 +717,6 @@ public class GenericClientTest {
assertEquals("name=" + longValue, string);
}
@Test
public void testLoadPageAndReturnDstu1Bundle() throws Exception {
String msg = getPatientFeedWithOneResult();
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")));
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
IGenericClient client = ourCtx.newRestfulGenericClient("http://foo");
client
.loadPage()
.byUrl("http://example.com/page1")
.andReturnBundle(Bundle.class)
.execute();
assertEquals("http://example.com/page1", capt.getValue().getURI().toString());
}
@Test
public void testSearchByCompartment() throws Exception {
@ -1023,49 +1120,6 @@ public class GenericClientTest {
}
@SuppressWarnings("unused")
@Test
public void testSearchByTokenWithSystemAndNoCode() throws Exception {
final String msg = getPatientFeedWithOneResult();
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() {
@Override
public InputStream answer(InvocationOnMock theInvocation) throws Throwable {
return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"));
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
int idx = 0;
Bundle response = client.search()
.forResource("Patient")
.where(Patient.IDENTIFIER.hasSystemWithAnyCode("urn:foo"))
.returnBundle(Bundle.class)
.execute();
assertEquals("http://example.com/fhir/Patient?identifier=urn%3Afoo%7C", capt.getAllValues().get(idx++).getURI().toString());
response = client.search()
.forResource("Patient")
.where(Patient.IDENTIFIER.exactly().systemAndCode("urn:foo", null))
.returnBundle(Bundle.class)
.execute();
assertEquals("http://example.com/fhir/Patient?identifier=urn%3Afoo%7C", capt.getAllValues().get(idx++).getURI().toString());
response = client.search()
.forResource("Patient")
.where(Patient.IDENTIFIER.exactly().systemAndCode("urn:foo", ""))
.returnBundle(Bundle.class)
.execute();
assertEquals("http://example.com/fhir/Patient?identifier=urn%3Afoo%7C", capt.getAllValues().get(idx++).getURI().toString());
}
/**
* Test for #192
*/
@ -1114,6 +1168,49 @@ public class GenericClientTest {
index++;
}
@SuppressWarnings("unused")
@Test
public void testSearchByTokenWithSystemAndNoCode() throws Exception {
final String msg = getPatientFeedWithOneResult();
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() {
@Override
public InputStream answer(InvocationOnMock theInvocation) throws Throwable {
return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"));
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
int idx = 0;
Bundle response = client.search()
.forResource("Patient")
.where(Patient.IDENTIFIER.hasSystemWithAnyCode("urn:foo"))
.returnBundle(Bundle.class)
.execute();
assertEquals("http://example.com/fhir/Patient?identifier=urn%3Afoo%7C", capt.getAllValues().get(idx++).getURI().toString());
response = client.search()
.forResource("Patient")
.where(Patient.IDENTIFIER.exactly().systemAndCode("urn:foo", null))
.returnBundle(Bundle.class)
.execute();
assertEquals("http://example.com/fhir/Patient?identifier=urn%3Afoo%7C", capt.getAllValues().get(idx++).getURI().toString());
response = client.search()
.forResource("Patient")
.where(Patient.IDENTIFIER.exactly().systemAndCode("urn:foo", ""))
.returnBundle(Bundle.class)
.execute();
assertEquals("http://example.com/fhir/Patient?identifier=urn%3Afoo%7C", capt.getAllValues().get(idx++).getURI().toString());
}
@SuppressWarnings("unused")
@Test
public void testSearchIncludeRecursive() throws Exception {
@ -1429,28 +1526,6 @@ public class GenericClientTest {
}
private Bundle createTransactionBundleOutput() {
Bundle output = new Bundle();
output.setType(BundleType.TRANSACTIONRESPONSE);
output
.addEntry()
.setResource(createPatientP1())
.getResponse()
.setLocation(createPatientP1().getId());
return output;
}
private Bundle createTransactionBundleInput() {
Bundle input = new Bundle();
input.setType(BundleType.TRANSACTION);
input
.addEntry()
.setResource(createPatientP1())
.getRequest()
.setMethod(HTTPVerb.POST);
return input;
}
@Test
public void testUpdate() throws Exception {
@ -1558,32 +1633,6 @@ public class GenericClientTest {
count++;
}
@Test
public void testValidateNonFluent() throws Exception {
OperationOutcome oo = new OperationOutcome();
oo.addIssue().setDiagnostics("OOOK");
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] {});
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(ourCtx.newXmlParser().encodeResourceToString(oo)), Charset.forName("UTF-8")));
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
Patient p1 = new Patient();
p1.addIdentifier().setSystem("foo:bar").setValue("12345");
p1.addName().setFamily("Smith").addGiven("John");
MethodOutcome resp = client.validate(p1);
assertEquals("http://example.com/fhir/Patient/$validate", capt.getValue().getURI().toString());
oo = (OperationOutcome) resp.getOperationOutcome();
assertEquals("OOOK", oo.getIssueFirstRep().getDiagnostics());
}
@Test
public void testVReadWithAbsoluteUrl() throws Exception {
@ -1613,9 +1662,30 @@ public class GenericClientTest {
}
@BeforeClass
public static void beforeClass() {
ourCtx = FhirContext.forR4();
@Test
public void testValidateNonFluent() throws Exception {
OperationOutcome oo = new OperationOutcome();
oo.addIssue().setDiagnostics("OOOK");
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] {});
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(ourCtx.newXmlParser().encodeResourceToString(oo)), Charset.forName("UTF-8")));
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
Patient p1 = new Patient();
p1.addIdentifier().setSystem("foo:bar").setValue("12345");
p1.addName().setFamily("Smith").addGiven("John");
MethodOutcome resp = client.validate(p1);
assertEquals("http://example.com/fhir/Patient/$validate", capt.getValue().getURI().toString());
oo = (OperationOutcome) resp.getOperationOutcome();
assertEquals("OOOK", oo.getIssueFirstRep().getDiagnostics());
}
@AfterClass
@ -1623,4 +1693,9 @@ public class GenericClientTest {
TestUtil.clearAllStaticFieldsForUnitTest();
}
@BeforeClass
public static void beforeClass() {
ourCtx = FhirContext.forR4();
}
}

View File

@ -27,6 +27,16 @@
has been changed to only accept "url".
Thanks to Avinash Shanbhag for reporting!
</action>
<action type="add">
JPA server now supports the use of the
<![CDATA[<code>Cache-Control</code>]]>
header in order to allow the client to selectively disable the
search result cache. This directive can also be used to disable result paging
and return results faster when only a small number of results is needed.
See the
<![CDATA[<a href="http://hapifhir.io/doc_jpa.html">JPA Page</a>]]>
for more information.
</action>
</release>
<release version="3.0.0" date="2017-09-27">
<action type="add">

View File

@ -198,13 +198,70 @@ public DaoConfig daoConfig() {
(see <a href="./apidocs-jpaserver/ca/uhn/fhir/jpa/dao/DaoConfig.html#setTreatReferencesAsLogical-java.util.Set-">JavaDoc</a>).
For example:
</p>
<code>
<div class="source">
<pre>
// Treat specific URL as logical
myDaoConfig.getTreatReferencesAsLogical().add("http://mysystem.com/ValueSet/cats-and-dogs");
// Treat all references with given prefix as logical
myDaoConfig.getTreatReferencesAsLogical().add("http://mysystem.com/mysystem-vs-*");
</code>
</pre>
</div>
<a name="search_result caching"/>
</subsection>
<subsection name="Search Result Caching">
<p>
By default, search results will be cached for one minute. This means that
if a client performs a search for <code>Patient?name=smith</code> and gets back
500 results, if a client performs the same search within 60000 milliseconds the
previously loaded search results will be returned again. This also means that
any new Patient resources named "Smith" within the last minute will not be
reflected in the results.
</p>
<p>
Under many normal scenarios this is a n acceptable performance tradeoff,
but in some cases it is not. If you want to disable caching, you have two
options:
</p>
<p><b>Globally Disable / Change Caching Timeout</b></p>
<p>
You can change the global cache using the following setting:
</p>
<div class="source">
<pre>
myDaoConfig.setReuseCachedSearchResultsForMillis(null);
</pre>
</div>
<p><b>Disable Cache at the Request Level</b></p>
<p>
Clients can selectively disable caching for an individual request
using the Cache-Control header:
</p>
<div class="source">
<pre>
Cache-Control: nocache
</pre>
</div>
<p><b>Disable Paging at the Request Level</b></p>
<p>
If the client knows that they will only want a small number of results
(for example, a UI containing 20 results is being shown and the client
knows that they will never load the next page of results) the client
may also use the <code>nostore</code> directive along with a HAPI FHIR
extension called <code>max-results</code> in order to specify that
only the given number of results should be fetched. This directive
disabled paging entirely for the request and causes the request to
return immediately when the given number of results is found. This
can cause a noticeable performance improvement in some cases.
</p>
<div class="source">
<pre>
Cache-Control: nostore, max-results=20
</pre>
</div>
</subsection>
</section>

View File

@ -534,6 +534,34 @@
</section>
<section name="Additional Properties">
<p>
This section contains ways of customizing the request sent by the client
</p>
<subsection name="Cache-Control">
<p>
The <code>Cache-Control</code> header can be used by the client in a request
to signal to the server (or any cache in front of it) that the client wants specific
behaviour from the cache, or wants the cache to not act on the request altogether.
Naturally, not all servers will honour this header.
</p>
<p>
To add a cache control directive in a request:
</p>
<macro name="snippet">
<param name="id" value="cacheControl" />
<param name="file" value="examples/src/main/java/example/ClientExamples.java" />
</macro>
</subsection>
</section>
</body>
</document>