From ce720f5601d6f27ac323b506fb83bba1a9858320 Mon Sep 17 00:00:00 2001
From: James
Date: Thu, 5 Oct 2017 13:38:53 -0400
Subject: [PATCH] Add support for Cache-Control header in JPA server and client
---
.../src/main/java/example/ClientExamples.java | 22 +
.../fhir/rest/api/CacheControlDirective.java | 108 +
.../java/ca/uhn/fhir/rest/api/Constants.java | 4 +
.../fhir/rest/gclient/IClientExecutable.java | 7 +
.../rest/api/CacheControlDirectiveTest.java | 58 +
.../uhn/fhir/rest/client/impl/BaseClient.java | 28 +-
.../fhir/rest/client/impl/GenericClient.java | 21 +-
.../fhir/jpa/dao/BaseHapiFhirResourceDao.java | 12 +-
.../java/ca/uhn/fhir/jpa/dao/DaoConfig.java | 45 +-
.../jpa/dao/FhirResourceDaoPatientDstu2.java | 2 +-
.../dstu3/FhirResourceDaoPatientDstu3.java | 3 +-
.../jpa/dao/r4/FhirResourceDaoPatientR4.java | 3 +-
.../jpa/search/ISearchCoordinatorSvc.java | 13 +-
.../jpa/search/SearchCoordinatorSvcImpl.java | 36 +-
.../r4/ResourceProviderR4CacheTest.java | 164 +
.../search/SearchCoordinatorSvcImplTest.java | 136 +-
.../fhir/rest/client/GenericClientTest.java | 3327 +++++++++--------
src/changes/changes.xml | 10 +
src/site/xdoc/doc_jpa.xml | 687 ++--
src/site/xdoc/doc_rest_client.xml | 1106 +++---
20 files changed, 3211 insertions(+), 2581 deletions(-)
create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/CacheControlDirective.java
create mode 100644 hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/api/CacheControlDirectiveTest.java
create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CacheTest.java
diff --git a/examples/src/main/java/example/ClientExamples.java b/examples/src/main/java/example/ClientExamples.java
index 1c263e23b0d..25a805d8dd8 100644
--- a/examples/src/main/java/example/ClientExamples.java
+++ b/examples/src/main/java/example/ClientExamples.java
@@ -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
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/CacheControlDirective.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/CacheControlDirective.java
new file mode 100644
index 00000000000..5d2c0595c86
--- /dev/null
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/CacheControlDirective.java
@@ -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 Cache-Control header called max-results=123
+ * 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 Cache-Control header called max-results=123
+ * 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 true<, adds the no-cache directive to the
+ * request. This directive indicates that the cache should not be used to
+ * serve this request.
+ */
+ public boolean isNoCache() {
+ return myNoCache;
+ }
+
+ /**
+ * If true<, adds the no-cache 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 Cache-Control header values
+ *
+ * @param theValues The Cache-Control header values
+ */
+ public CacheControlDirective parse(List 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;
+ }
+}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java
index 4d067c33677..ab94dd399ba 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java
@@ -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";
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IClientExecutable.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IClientExecutable.java
index b345a6d02b2..bb08eb5a677 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IClientExecutable.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IClientExecutable.java
@@ -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, Y> {
@Deprecated
T andLogRequestAndResponse(boolean theLogRequestAndResponse);
+ /**
+ * Sets the Cache-Control 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: subsetElements("name", "identifier") requests that the server only return
diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/api/CacheControlDirectiveTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/api/CacheControlDirectiveTest.java
new file mode 100644
index 00000000000..4591b4bc1f1
--- /dev/null
+++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/api/CacheControlDirectiveTest.java
@@ -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 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 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 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 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());
+ }
+}
diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/BaseClient.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/BaseClient.java
index 4aa1e2770df..86594930586 100644
--- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/BaseClient.java
+++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/BaseClient.java
@@ -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 fetchResourceFromUrl(Class theResourceType, String theUrl) {
BaseHttpClientInvocation clientInvocation = new HttpGetClientInvocation(getFhirContext(), theUrl);
ResourceResponseHandler binding = new ResourceResponseHandler(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 invokeClient(FhirContext theContext, IClientResponseHandler 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 invokeClient(FhirContext theContext, IClientResponseHandler binding, BaseHttpClientInvocation clientInvocation, EncodingEnum theEncoding, Boolean thePrettyPrint,
- boolean theLogRequestAndResponse, SummaryEnum theSummaryMode, Set theSubsetElements) {
+ boolean theLogRequestAndResponse, SummaryEnum theSummaryMode, Set 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!
*/
diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/GenericClient.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/GenericClient.java
index c82d329a18e..60687a5f7b9 100644
--- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/GenericClient.java
+++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/GenericClient.java
@@ -120,10 +120,10 @@ public class GenericClient extends BaseClient implements IGenericClient {
ResourceResponseHandler binding = new ResourceResponseHandler(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 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 invoke(Map> theParams, IClientResponseHandler 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;
}
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java
index a234e456941..2c483a06092 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java
@@ -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 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
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java
index 1e972721644..0d25bd2dfee 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java
@@ -107,6 +107,7 @@ public class DaoConfig {
private Set myTreatBaseUrlsAsLocal = new HashSet();
private Set myTreatReferencesAsLogical = new HashSet(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
+ * Cache-Control: nostore, max-results=NNN
+ * 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
+ * Cache-Control: nostore, max-results=NNN
+ * 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 theInterceptors) {
- myInterceptors = theInterceptors;
+ public void setInterceptors(IServerInterceptor... theInterceptor) {
+ setInterceptors(new ArrayList());
+ 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.
*
+ *
+ * Note that if this is set to a non-null value, clients may override this setting by using
+ * the Cache-Control header. If this is set to null, the Cache-Control
+ * header will be ignored.
+ *
*/
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.
*
+ *
+ * Note that if this is set to a non-null value, clients may override this setting by using
+ * the Cache-Control header. If this is set to null, the Cache-Control
+ * header will be ignored.
+ *
*/
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());
- if (theInterceptor != null && theInterceptor.length != 0) {
- getInterceptors().addAll(Arrays.asList(theInterceptor));
- }
+ public void setInterceptors(List theInterceptors) {
+ myInterceptors = theInterceptors;
}
/**
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoPatientDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoPatientDstu2.java
index 04932c11d79..4def0078564 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoPatientDstu2.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoPatientDstu2.java
@@ -65,7 +65,7 @@ public class FhirResourceDaoPatientDstu2 extends FhirResourceDaoDstu2im
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
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoPatientDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoPatientDstu3.java
index fed35e6ad2a..31e320b04aa 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoPatientDstu3.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoPatientDstu3.java
@@ -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 FhirResourceDaoDstu3im
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
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoPatientR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoPatientR4.java
index f9d66c30f7a..3d10dfcf1a4 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoPatientR4.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoPatientR4.java
@@ -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 FhirResourceDaoR4implemen
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
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/ISearchCoordinatorSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/ISearchCoordinatorSvc.java
index 72ecc96cfdd..2ae40cc0665 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/ISearchCoordinatorSvc.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/ISearchCoordinatorSvc.java
@@ -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 getResources(String theUuid, int theFrom, int theTo);
-
- IBundleProvider registerSearch(IDao theCallingDao, SearchParameterMap theParams, String theResourceType);
-
void cancelAllActiveSearches();
+ List getResources(String theUuid, int theFrom, int theTo);
+
+ IBundleProvider registerSearch(IDao theCallingDao, SearchParameterMap theParams, String theResourceType, CacheControlDirective theCacheControlDirective);
+
}
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java
index e2e9cb30da6..4061859ce46 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java
@@ -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 myIdToSearchTask = new ConcurrentHashMap();
@Autowired
private FhirContext myContext;
@Autowired
@@ -63,7 +65,6 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
@Autowired
private EntityManager myEntityManager;
private ExecutorService myExecutor;
- private final ConcurrentHashMap myIdToSearchTask = new ConcurrentHashMap();
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 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 {
- 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 mySyncedPids = new ArrayList();
private final ArrayList myUnsyncedPids = new ArrayList();
+ private boolean myAbortRequested;
+ private int myCountSaved = 0;
private String mySearchUuid;
public SearchTask(Search theSearch, IDao theCallingDao, SearchParameterMap theParams, String theResourceType, String theSearchUuid) {
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CacheTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CacheTest.java
new file mode 100644
index 00000000000..24373a8ce46
--- /dev/null
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CacheTest.java
@@ -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();
+ }
+
+}
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImplTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImplTest.java
index 2720e48f8f3..53453dd284d 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImplTest.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImplTest.java
@@ -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;
-@SuppressWarnings({ "unchecked" })
+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> mySearchResultIterCaptor;
@Mock
private IDao myCallingDao;
@Mock
@@ -49,10 +68,6 @@ public class SearchCoordinatorSvcImplTest {
private ISearchIncludeDao mySearchIncludeDao;
@Mock
private ISearchResultDao mySearchResultDao;
- @Captor
- ArgumentCaptor> mySearchResultIterCaptor;
-
-
private SearchCoordinatorSvcImpl mySvc;
@Mock
@@ -63,9 +78,10 @@ public class SearchCoordinatorSvcImplTest {
public void after() {
verify(myCallingDao, atMost(myExpectedNumberOfSearchBuildersCreated)).newSearchBuilder();
}
+
@Before
public void before() {
-
+
mySvc = new SearchCoordinatorSvcImpl();
mySvc.setEntityManagerForUnitTest(myEntityManager);
mySvc.setTransactionManagerForUnitTest(myTxManager);
@@ -76,9 +92,9 @@ public class SearchCoordinatorSvcImplTest {
myDaoConfig = new DaoConfig();
mySvc.setDaoConfigForUnitTest(myDaoConfig);
-
+
when(myCallingDao.newSearchBuilder()).thenReturn(mySearchBuider);
-
+
doAnswer(new Answer() {
@Override
public Void answer(InvocationOnMock theInvocation) throws Throwable {
@@ -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 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,12 +168,12 @@ 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());
List resources;
-
+
resources = result.getResources(0, 100000);
assertEquals(790, resources.size());
assertEquals("10", resources.get(0).getIdElement().getValueAsString());
@@ -164,18 +181,18 @@ public class SearchCoordinatorSvcImplTest {
ArgumentCaptor searchCaptor = ArgumentCaptor.forClass(Search.class);
verify(mySearchDao, atLeastOnce()).save(searchCaptor.capture());
-
+
verify(mySearchResultDao, atLeastOnce()).save(mySearchResultIterCaptor.capture());
- List allResults= new ArrayList();
+ List allResults = new ArrayList();
for (Iterable next : mySearchResultIterCaptor.getAllValues()) {
allResults.addAll(Lists.newArrayList(next));
}
-
+
assertEquals(790, allResults.size());
assertEquals(10, allResults.get(0).getResourcePid().longValue());
assertEquals(799, allResults.get(789).getResourcePid().longValue());
}
-
+
@Test
public void testAsyncSearchLargeResultSetSameCoordinator() {
SearchParameterMap params = new SearchParameterMap();
@@ -187,12 +204,12 @@ 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());
List resources;
-
+
resources = result.getResources(0, 30);
assertEquals(30, resources.size());
assertEquals("10", resources.get(0).getIdElement().getValueAsString());
@@ -202,7 +219,7 @@ public class SearchCoordinatorSvcImplTest {
/**
* Subsequent requests for the same search (i.e. a request for the next
- * page) within the same JVM will not use the original bundle provider
+ * page) within the same JVM will not use the original bundle provider
*/
@Test
public void testAsyncSearchLargeResultSetSecondRequestSameCoordinator() {
@@ -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());
@@ -223,10 +240,10 @@ public class SearchCoordinatorSvcImplTest {
verify(mySearchDao, atLeast(1)).save(searchCaptor.capture());
Search search = searchCaptor.getValue();
assertEquals(SearchTypeEnum.SEARCH, search.getSearchType());
-
+
List resources;
PersistedJpaBundleProvider provider;
-
+
resources = result.getResources(0, 10);
assertNull(result.size());
assertEquals(10, resources.size());
@@ -244,7 +261,7 @@ public class SearchCoordinatorSvcImplTest {
assertEquals(10, resources.size());
assertEquals("20", resources.get(0).getIdElement().getValueAsString());
assertEquals("29", resources.get(9).getIdElement().getValueAsString());
-
+
provider = new PersistedJpaBundleProvider(result.getUuid(), myCallingDao);
resources = provider.getResources(20, 99999);
assertEquals(770, resources.size());
@@ -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());
@@ -285,18 +302,18 @@ public class SearchCoordinatorSvcImplTest {
@Test
public void testLoadSearchResultsFromDifferentCoordinator() {
final String uuid = UUID.randomUUID().toString();
-
+
final Search search = new Search();
search.setUuid(uuid);
search.setSearchType(SearchTypeEnum.SEARCH);
search.setResourceType("Patient");
-
+
when(mySearchDao.findByUuid(eq(uuid))).thenReturn(search);
doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(List.class), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao));
PersistedJpaBundleProvider provider;
List resources;
-
+
new Thread() {
@Override
public void run() {
@@ -305,20 +322,21 @@ public class SearchCoordinatorSvcImplTest {
} catch (InterruptedException e) {
// ignore
}
-
+
when(mySearchResultDao.findWithSearchUuid(any(Search.class), any(Pageable.class))).thenAnswer(new Answer>() {
@Override
public Page answer(InvocationOnMock theInvocation) throws Throwable {
Pageable page = (Pageable) theInvocation.getArguments()[1];
-
+
ArrayList results = new ArrayList();
int max = (page.getPageNumber() * page.getPageSize()) + page.getPageSize();
for (int i = page.getOffset(); i < max; i++) {
results.add(new SearchResult().setResourcePid(i + 10L));
}
-
+
return new PageImpl(results);
- }});
+ }
+ });
search.setStatus(SearchStatusEnum.FINISHED);
}
}.start();
@@ -332,7 +350,7 @@ public class SearchCoordinatorSvcImplTest {
assertEquals(10, resources.size());
assertEquals("20", resources.get(0).getIdElement().getValueAsString());
assertEquals("29", resources.get(9).getIdElement().getValueAsString());
-
+
provider = new PersistedJpaBundleProvider(uuid, myCallingDao);
resources = provider.getResources(20, 40);
assertEquals(20, resources.size());
@@ -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());
@@ -394,7 +412,7 @@ public class SearchCoordinatorSvcImplTest {
private int myCount;
private Iterator myWrap;
-
+
public FailAfterNIterator(Iterator theWrap, int theCount) {
myWrap = theWrap;
myCount = theCount;
@@ -416,7 +434,7 @@ public class SearchCoordinatorSvcImplTest {
}
-
+
public static class SlowIterator extends BaseIterator implements Iterator {
private int myDelay;
diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/GenericClientTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/GenericClientTest.java
index 5c13093d826..34579616a06 100644
--- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/GenericClientTest.java
+++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/GenericClientTest.java
@@ -1,1626 +1,1701 @@
-package ca.uhn.fhir.rest.client;
-
-import static org.hamcrest.Matchers.containsString;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertThat;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import java.io.*;
-import java.net.URLEncoder;
-import java.nio.charset.Charset;
-import java.util.*;
-
-import org.apache.commons.io.IOUtils;
-import org.apache.commons.io.input.ReaderInputStream;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.http.*;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.entity.UrlEncodedFormEntity;
-import org.apache.http.client.methods.*;
-import org.apache.http.message.BasicHeader;
-import org.apache.http.message.BasicStatusLine;
-import org.hamcrest.Matchers;
-import org.hamcrest.core.StringContains;
-import org.hl7.fhir.instance.model.api.IBaseResource;
-import org.hl7.fhir.r4.model.*;
-import org.hl7.fhir.r4.model.Bundle.BundleType;
-import org.hl7.fhir.r4.model.Bundle.HTTPVerb;
-import org.junit.*;
-import org.mockito.ArgumentCaptor;
-import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-import ca.uhn.fhir.context.FhirContext;
-import ca.uhn.fhir.model.api.*;
-import ca.uhn.fhir.model.primitive.InstantDt;
-import ca.uhn.fhir.model.primitive.UriDt;
-import ca.uhn.fhir.rest.api.*;
-import ca.uhn.fhir.rest.api.Constants;
-import ca.uhn.fhir.rest.client.api.IGenericClient;
-import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
-import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException;
-import ca.uhn.fhir.rest.client.impl.BaseClient;
-import ca.uhn.fhir.rest.client.impl.GenericClient;
-import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
-import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
-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 HttpClient myHttpClient;
-
- private HttpResponse myHttpResponse;
-
- @Before
- public void before() {
-
- myHttpClient = mock(HttpClient.class, new ReturnsDeepStubs());
- ourCtx.getRestfulClientFactory().setHttpClient(myHttpClient);
- ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
-
- myHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs());
-
- System.setProperty(BaseClient.HAPI_CLIENT_KEEPRESPONSES, "true");
- }
-
- private String extractBody(ArgumentCaptor 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
-// String msg = "\n" +
-// "\n" +
-// "d039f91a-cc3c-4013-988e-af4d8d0614bd\n" +
-// "1\n" +
-// "\n" +
-// "ca.uhn.fhir.rest.server.DummyRestfulServer\n" +
-// "\n" +
-// "\n" +
-// ""
-// + ""
-// + "
- The HAPI FHIR
- RestfulServer
- module can be used to create a FHIR server endpoint against an arbitrary
- data source, which could be a database of your own design, an existing
- clinical system, a set of files, or anything else you come up with.
-
-
- HAPI also provides a persistence module which can be used to
- provide a complete RESTful server implementation, backed by a database of
- your choosing. This module uses the JPA 2.0
- API to store data in a database without depending on any specific database technology.
-
-
- Important Note:
- This implementation uses a fairly simple table design, with a
- single table being used to hold resource bodies (which are stored as
- CLOBs, optionally GZipped to save space) and a set of tables to hold search indexes, tags,
- history details, etc. This design is only one of many possible ways
- of designing a FHIR server so it is worth considering whether it
- is appropriate for the problem you are trying to solve.
-
-
-
-
-
- The easiest way to get started with HAPI's JPA server module is
- to begin with the example project. There is a complete sample project
- found in our GitHub repo here: hapi-fhir-jpaserver-example
-
-
-
- This example is a fully contained FHIR server, supporting all standard operations (read/create/delete/etc).
- It bundles an embedded instance of the Apache Derby Java database
- so that the server can run without depending on any external database, but it can also be
- configured to use an installation of Oracle, Postgres, etc.
-
-
-
- To take this project for a spin, check out the sources from GitHib (or download a snapshot),
- and then build the project:
-
-
-
-
-
- You now have two options for starting the server:
-
-
-
- Deploy to Tomcat/JBoss/Websphere/etc: You will now have a file
- in your target directory called hapi-fhir-jpaserver-example.war.
- This WAR file can be deployed to any Servlet container, at which point you could
- access the server by pointing your browser at a URL similar to the following
- (you may need to adjust the
- port depending on which port your container is configured to listen on):
- http://localhost:8080/hapi-fhir-jpaserver-example/
-
-
- Run with Maven and Embedded Jetty: To start the server
- directly within Maven, you can execute the following command:
-
- You can then access the server by pointing your browser at the following URL:
- http://localhost:8080/hapi-fhir-jpaserver-example/
-
-
-
-
-
-
-
-
- The JPA server is configured through a series of configuration files, most
- of which are documented inline.
-
- The Spring confguration contains a definition for a bean called daoConfig,
- which will look something like the following:
-
-
-
-
- You can use this method to change various configuration settings on the DaoConfig bean
- which define the way that the JPA server will behave.
- See the DaoConfig JavaDoc
- for information about the available settings.
-
-
-
-
-
- Clients may sometimes post resources to your server that contain
- absolute resource references. For example, consider the following resource:
-
- ]]>
-
-
- By default, the server will reject this reference, as only
- local references are permitted by the server. This can be changed
- however.
-
-
- If you want the server to recognize that this URL is actually a local
- reference (i.e. because the server will be deployed to the base URL
- http://example.com/fhir/) you can
- configure the server to recognize this URL via the following DaoConfig
- setting:
-
-
-
-
- On the other hand, if you want the server to be configurable to
- allow remote references, you can set this with the confguration below.
- Using the setAllowExternalReferences means that
- it will be possible to search for references that refer to these
- external references.
-
-
-
-
-
-
-
-
- In some cases, you may have references which are Logical References,
- which means that they act as an identifier and not necessarily as a literal
- web address.
-
-
- A common use for logical references is in references to conformance
- resources, such as ValueSets, StructureDefinitions, etc. For example,
- you might refer to the ValueSet
- http://hl7.org/fhir/ValueSet/quantity-comparator
- from your own resources. In this case, you are not neccesarily telling
- the server that this is a real address that it should resolve, but
- rather that this is an identifier for a ValueSet where
- ValueSet.url has the given URI/URL.
-
-
- HAPI can be configured to treat certain URI/URL patterns as
- logical by using the DaoConfig#setTreatReferencesAsLogical property
- (see JavaDoc).
- For example:
-
-
- // 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-*");
-
-
-
-
-
-
-
-
-
-
- The HAPI JPA Server has the following components:
-
-
-
-
- Resource Providers:
- A RESTful server Resource Provider is
- provided for each resource type in a given release of FHIR. Each resource provider implements
- a
- @Search
- method implementing the complete set of search parameters defined in the FHIR
- specification for the given resource type.
- The resource providers also extend a superclass which implements all of the
- other FHIR methods, such as Read, Create, Delete, etc.
- Note that these resource providers are generated as a part of the HAPI build process,
- so they are not checked into Git. You can see their source
- in the JXR Report,
- for example the
- PatientResourceProvider.
-
- The resource providers do not actually implement any of the logic
- in searching, updating, etc. They simply receive the incoming HTTP calls (via the RestfulServer)
- and pass along the incoming requests to the DAOs.
-
-
-
- HAPI DAOs:
- The DAOs actually implement all of the database business logic relating to
- the storage, indexing, and retrieval of FHIR resources, using the underlying JPA
- API.
-
-
-
- Hibernate:
- The HAPI JPA Server uses the JPA library, implemented by Hibernate. No Hibernate
- specific features are used, so the library should also work with other
- providers (e.g. Eclipselink) but it is not tested regularly with them.
-
-
-
- Database:
- The RESTful server uses an embedded Derby database, but can be configured to
- talk to
- any database supported by Hibernate.
-
-
-
-
-
-
-
-
-
-
- This page
- has information on loading national editions (UK specifically) of SNOMED CT files into
- the database.
-
+ The HAPI FHIR
+ RestfulServer
+ module can be used to create a FHIR server endpoint against an arbitrary
+ data source, which could be a database of your own design, an existing
+ clinical system, a set of files, or anything else you come up with.
+
+
+ HAPI also provides a persistence module which can be used to
+ provide a complete RESTful server implementation, backed by a database of
+ your choosing. This module uses the JPA 2.0
+ API to store data in a database without depending on any specific database technology.
+
+
+ Important Note:
+ This implementation uses a fairly simple table design, with a
+ single table being used to hold resource bodies (which are stored as
+ CLOBs, optionally GZipped to save space) and a set of tables to hold search indexes, tags,
+ history details, etc. This design is only one of many possible ways
+ of designing a FHIR server so it is worth considering whether it
+ is appropriate for the problem you are trying to solve.
+
+
+
+
+
+ The easiest way to get started with HAPI's JPA server module is
+ to begin with the example project. There is a complete sample project
+ found in our GitHub repo here: hapi-fhir-jpaserver-example
+
+
+
+ This example is a fully contained FHIR server, supporting all standard operations (read/create/delete/etc).
+ It bundles an embedded instance of the Apache Derby Java database
+ so that the server can run without depending on any external database, but it can also be
+ configured to use an installation of Oracle, Postgres, etc.
+
+
+
+ To take this project for a spin, check out the sources from GitHib (or download a snapshot),
+ and then build the project:
+
+
+
+
+
+ You now have two options for starting the server:
+
+
+
+ Deploy to Tomcat/JBoss/Websphere/etc: You will now have a file
+ in your target directory called hapi-fhir-jpaserver-example.war.
+ This WAR file can be deployed to any Servlet container, at which point you could
+ access the server by pointing your browser at a URL similar to the following
+ (you may need to adjust the
+ port depending on which port your container is configured to listen on):
+ http://localhost:8080/hapi-fhir-jpaserver-example/
+
+
+ Run with Maven and Embedded Jetty: To start the server
+ directly within Maven, you can execute the following command:
+
+ You can then access the server by pointing your browser at the following URL:
+ http://localhost:8080/hapi-fhir-jpaserver-example/
+
+
+
+
+
+
+
+
+ The JPA server is configured through a series of configuration files, most
+ of which are documented inline.
+
+ The Spring confguration contains a definition for a bean called daoConfig,
+ which will look something like the following:
+
+
+
+
+ You can use this method to change various configuration settings on the DaoConfig bean
+ which define the way that the JPA server will behave.
+ See the DaoConfig JavaDoc
+ for information about the available settings.
+
+
+
+
+
+ Clients may sometimes post resources to your server that contain
+ absolute resource references. For example, consider the following resource:
+
+ ]]>
+
+
+ By default, the server will reject this reference, as only
+ local references are permitted by the server. This can be changed
+ however.
+
+
+ If you want the server to recognize that this URL is actually a local
+ reference (i.e. because the server will be deployed to the base URL
+ http://example.com/fhir/) you can
+ configure the server to recognize this URL via the following DaoConfig
+ setting:
+
+
+
+
+ On the other hand, if you want the server to be configurable to
+ allow remote references, you can set this with the confguration below.
+ Using the setAllowExternalReferences means that
+ it will be possible to search for references that refer to these
+ external references.
+
+
+
+
+
+
+
+
+ In some cases, you may have references which are Logical References,
+ which means that they act as an identifier and not necessarily as a literal
+ web address.
+
+
+ A common use for logical references is in references to conformance
+ resources, such as ValueSets, StructureDefinitions, etc. For example,
+ you might refer to the ValueSet
+ http://hl7.org/fhir/ValueSet/quantity-comparator
+ from your own resources. In this case, you are not neccesarily telling
+ the server that this is a real address that it should resolve, but
+ rather that this is an identifier for a ValueSet where
+ ValueSet.url has the given URI/URL.
+
+
+ HAPI can be configured to treat certain URI/URL patterns as
+ logical by using the DaoConfig#setTreatReferencesAsLogical property
+ (see JavaDoc).
+ For example:
+
+
+
+ // 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-*");
+
+
+
+
+
+
+
+
+ By default, search results will be cached for one minute. This means that
+ if a client performs a search for Patient?name=smith 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.
+
+
+ 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:
+
+
Globally Disable / Change Caching Timeout
+
+ You can change the global cache using the following setting:
+
+ Clients can selectively disable caching for an individual request
+ using the Cache-Control header:
+
+
+
+ Cache-Control: nocache
+
+
+
Disable Paging at the Request Level
+
+ 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 nostore directive along with a HAPI FHIR
+ extension called max-results 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.
+
+
+
+ Cache-Control: nostore, max-results=20
+
+
+
+
+
+
+
+
+
+
+
+
+ The HAPI JPA Server has the following components:
+
+
+
+
+ Resource Providers:
+ A RESTful server Resource Provider is
+ provided for each resource type in a given release of FHIR. Each resource provider implements
+ a
+ @Search
+ method implementing the complete set of search parameters defined in the FHIR
+ specification for the given resource type.
+ The resource providers also extend a superclass which implements all of the
+ other FHIR methods, such as Read, Create, Delete, etc.
+ Note that these resource providers are generated as a part of the HAPI build process,
+ so they are not checked into Git. You can see their source
+ in the JXR Report,
+ for example the
+ PatientResourceProvider.
+
+ The resource providers do not actually implement any of the logic
+ in searching, updating, etc. They simply receive the incoming HTTP calls (via the RestfulServer)
+ and pass along the incoming requests to the DAOs.
+
+
+
+ HAPI DAOs:
+ The DAOs actually implement all of the database business logic relating to
+ the storage, indexing, and retrieval of FHIR resources, using the underlying JPA
+ API.
+
+
+
+ Hibernate:
+ The HAPI JPA Server uses the JPA library, implemented by Hibernate. No Hibernate
+ specific features are used, so the library should also work with other
+ providers (e.g. Eclipselink) but it is not tested regularly with them.
+
+
+
+ Database:
+ The RESTful server uses an embedded Derby database, but can be configured to
+ talk to
+ any database supported by Hibernate.
+
+
+
+
+
+
+
+
+
+
+ This page
+ has information on loading national editions (UK specifically) of SNOMED CT files into
+ the database.
+
- HAPI provides a built-in mechanism for connecting to FHIR RESTful
- servers.
- The HAPI RESTful client is designed to be easy to set up and
- to allow strong
- compile-time type checking wherever possible.
-
-
-
- There are two types of RESTful clients provided by HAPI:
- The Fluent/Generic client (described below) and
- the Annotation
- client.
- The generic client is simpler to use
- and generally provides the faster way to get started. The annotation-driven
- client relies on static binding to specific operations to
- give better compile-time checking against servers with a specific set of capabilities
- exposed. This second model takes more effort to use, but can be useful
- if the person defining the specific methods to invoke is not the same person
- who is using those methods.
-
-
-
-
-
-
-
- Creating a generic client simply requires you to create an instance of
- FhirContext and use that to instantiate a client.
-
-
- The following example shows how to create a client, and a few operations which
- can be performed.
-
-
-
-
-
-
-
-
- Performance Tip: Note that FhirContext is an expensive object to create,
- so you should try to keep an instance around for the lifetime of your application. It
- is thread-safe so it can be passed as needed. Client instances, on the other hand,
- are very inexpensive to create so you can create a new one for each request if needed
- (although there is no requirement to do so, clients are reusable and thread-safe as well).
-
-
-
-
- The generic client supports queries using a fluent interface
- which is inspired by the fantastic
- .NET FHIR API.
- The fluent interface allows you to construct powerful queries by chaining
- method calls together, leading to highly readable code. It also allows
- you to take advantage of intellisense/code completion in your favourite
- IDE.
-
-
- Note that most fluent operations end with an execute()
- statement which actually performs the invocation. You may also invoke
- several configuration operations just prior to the execute() statement,
- such as encodedJson() or encodedXml().
-
-
-
-
-
-
- Searching for resources is probably the most common initial scenario for
- client applications, so we'll start the demonstration there. The FHIR search
- operation generally uses a URL with a set of predefined search parameters,
- and returns a Bundle containing zero-or-more resources which matched the
- given search criteria.
-
-
- Search is a very powerful mechanism, with advanced features such as paging,
- including linked resources, etc. See the FHIR
- search specification
- for more information.
-
-
-
- Note on Bundle types: As of DSTU2, FHIR defines Bundle as a resource
- instead of an Atom feed as it was in DSTU1. In code that was written for
- DSTU1 you would typically use the ca.uhn.fhir.model.api.Bundle
- class to represent a bundle, and that is that default return type for search
- methods. If you are implemeting a DSTU2+ server, is recommended to use a
- Bundle resource class instead (e.g. ca.uhn.fhir.model.dstu2.resource.Bundle
- or org.hl7.fhir.instance.model.Bundle). Many of the examples below include
- a chained invocation similar to
- .returnBundle(ca.uhn.fhir.model.dstu2.resource.Bundle.class), which
- instructs the search method which bundle type should be returned.
-
-
-
- The following example shows how to query using the generic client:
-
-
-
-
-
-
-
Search - Multi-valued Parameters (ANY/OR)
-
- To search for a set of possible values where ANY should be matched,
- you can provide multiple values to a parameter, as shown in the example below.
- This leads to a URL resembling ?family=Smith,Smyth
-
-
-
-
-
-
-
Search - Multi-valued Parameters (ALL/AND)
-
- To search for a set of possible values where ALL should be matched,
- you can provide multiple instances of a parameter, as shown in the example below.
- This leads to a URL resembling ?address=Toronto&address=Ontario&address=Canada
-
-
-
-
-
-
-
Search - Paging
-
- If the server supports paging results, the client has a page method
- which can be used to load subsequent pages.
-
-
-
-
-
-
-
Search - Composite Parameters
-
- If a composite parameter is being searched on, the parameter
- takes a "left" and "right" operand, each of which is
- a parameter from the resource being seached. The following example shows the
- syntax.
-
-
-
-
-
-
-
Search - By plain URL
-
- You can also perform a search using a String URL, instead
- of using the fluent method calls to build the URL. This
- can be useful if you have a URL you retrieved from
- somewhere else that you want to use as a search.
-
-
-
-
-
-
-
Search - Other Query Options
-
- The fluent search also has methods for sorting, limiting, specifying
- JSON encoding, _include, _revinclude, _lastUpdated, _tag, etc.
-
-
-
-
-
-
-
Search - Using HTTP POST
-
- The FHIR specification allows the use of an HTTP POST to transmit a search to a server instead of
- using
- an HTTP GET. With this style of search, the search parameters are included in the request body
- instead
- of the request URL, which can be useful if you need to transmit a search with a large number
- of parameters.
-
-
- The usingStyle() method controls which style to use. By default, GET style is used
- unless the client detects that the request would result in a very long URL (over 8000 chars) in which
- case the client automatically switches to POST.
-
-
- An alternate form of the search URL (using a URL ending with_search) was also
- supported in FHIR DSTU1. This form is no longer valid in FHIR DSTU2, but HAPI retains support
- for using this form in order to interoperate with servers which use it.
-
-
-
-
-
-
-
Search - Compartments
-
- To search a
- resource compartment,
- simply use the withIdAndCompartment
- method in your search.
-
-
-
-
-
-
-
Search - Subsetting (_summary and _elements)
-
- Sometimes you may want to only ask the server to include some parts of returned
- resources (instead of the whole resource). Typically this is for performance or
- optimization reasons, but there may also be privacy reasons for doing this.
-
-
- To request that the server return only "summary" elements (those elements
- defined in the specification with the "Σ" flag), you can use the
- summaryMode(SummaryEnum) qualifier:
-
-
-
-
-
-
- To request that the server return only elements from a custom list
- provided by the client, you can use the elementsSubset(String...)
- qualifier:
-
-
-
-
-
-
-
-
-
-
- The following example shows how to perform a create
- operation using the generic client:
-
-
-
-
-
-
-
Conditional Creates
-
- FHIR also specifies a type of update called "conditional create", where
- a set of search parameters are provided and a new resource is only
- created if no existing resource matches those parameters. See the
- FHIR specification for more information on conditional creation.
-
-
-
-
-
-
-
-
-
- Given a resource name and ID, it is simple to retrieve
- the latest version of that resource (a 'read')
-
-
-
-
-
-
- By adding a version string, it is also possible to retrieve a
- specific version (a 'vread')
-
-
-
-
-
-
- It is also possible to retrieve a resource given its absolute
- URL (this will override the base URL set on the client)
-
-
-
-
-
-
-
- See also the page on
- ETag Support
- for information on specifying a matching version in the
- client request.
-
-
-
-
-
-
- The following example shows how to perform a delete
- operation using the generic client:
-
-
-
-
-
-
Conditional Deletes
-
- Conditional deletions are also possible, which is a form where
- instead of deleting a resource using its logical ID, you specify
- a set of search criteria and a single resource is deleted if
- it matches that criteria. Note that this is not a mechanism
- for bulk deletion; see the FHIR specification for information
- on conditional deletes and how they are used.
-
-
-
-
-
-
-
-
-
- Updating a resource is similar to creating one, except that
- an ID must be supplied since you are updating a previously
- existing resource instance.
-
-
- The following example shows how to perform an update
- operation using the generic client:
-
-
-
-
-
-
-
Conditional Updates
-
- FHIR also specifies a type of update called "conditional updates", where
- insetad of using the logical ID of a resource to update, a set of
- search parameters is provided. If a single resource matches that set of
- parameters, that resource is updated. See the FHIR specification for
- information on how conditional updates work.
-
-
-
-
-
-
-
ETags and Resource Contention
-
- See also the page on
- ETag Support
- for information on specifying a matching version in the
- client request.
-
-
-
-
-
-
- To retrieve the version history of all resources, or all resources of a given type, or
- of a specific instance of a resource, you call the history()
- method.
-
-
-
-
-
-
-
- If you are using a DSTU2 compliant server, you should instead use the
- Bundle resource which is found in the DSTU2 structures JAR, as shown
- in the syntax below. Note that in both cases, the class name is Bundle,
- but the DSTU2 bundle is found in the .resources. package.
-
-
-
-
-
-
-
- You can also optionally request that only resource versions
- later than a given date, and/or only up to a given count (number)
- of resource versions be returned.
-
-
-
-
-
-
-
-
-
- The following example shows how to execute a transaction using the generic client:
-
-
-
-
-
-
-
-
-
- To retrieve the server's conformance statement, simply call the conformance()
- method as shown below.
-
-
-
-
-
-
-
-
-
-
- In the FHIR DSTU2 version, operations (referred to as "extended operations")
- were added. These operations are an RPC style of invocation, with a set of
- named input parameters passed to the server and a set of named output
- parameters returned back.
-
-
- To invoke an operation using the client, you simply need to create the
- input
- Parameters
- resource, then pass that to the operation() fluent method.
-
-
- The example below shows a simple operation call.
-
-
-
-
-
-
-
- Note that if the operation does not require any input parameters,
- you may also invoke the operation using the following form. Note that
- the withNoParameters still requires you to provide the
- type of the Parameters resource so that it can return the correct type in
- the response.
-
-
-
-
-
-
-
-
- By default, the client will invoke operations using the HTTP POST form.
- The FHIR specification also allows requests to use the HTTP GET verb
- if the operation is idempotent and has no composite/resource parameters.
- Use the following form to invoke operation with HTTP GET.
-
-
-
-
-
-
-
-
-
- The $validate operation asks the server to test a given resource
- to see if it would be acceptable as a create/update on that server.
- The client has built-in support for this operation.
-
-
- If the client is in DSTU1 mode, the method below will invoke the
- DSTU1 validation style instead.
-
-
-
-
-
-
-
-
-
-
-
- The $process-message operation asks the server to accept a fhir
- message bundle for processing.
-
+ HAPI provides a built-in mechanism for connecting to FHIR RESTful
+ servers.
+ The HAPI RESTful client is designed to be easy to set up and
+ to allow strong
+ compile-time type checking wherever possible.
+
+
+
+ There are two types of RESTful clients provided by HAPI:
+ The Fluent/Generic client (described below) and
+ the Annotation
+ client.
+ The generic client is simpler to use
+ and generally provides the faster way to get started. The annotation-driven
+ client relies on static binding to specific operations to
+ give better compile-time checking against servers with a specific set of capabilities
+ exposed. This second model takes more effort to use, but can be useful
+ if the person defining the specific methods to invoke is not the same person
+ who is using those methods.
+
+
+
+
+
+
+
+ Creating a generic client simply requires you to create an instance of
+ FhirContext and use that to instantiate a client.
+
+
+ The following example shows how to create a client, and a few operations which
+ can be performed.
+
+
+
+
+
+
+
+
+ Performance Tip: Note that FhirContext is an expensive object to create,
+ so you should try to keep an instance around for the lifetime of your application. It
+ is thread-safe so it can be passed as needed. Client instances, on the other hand,
+ are very inexpensive to create so you can create a new one for each request if needed
+ (although there is no requirement to do so, clients are reusable and thread-safe as well).
+
+
+
+
+ The generic client supports queries using a fluent interface
+ which is inspired by the fantastic
+ .NET FHIR API.
+ The fluent interface allows you to construct powerful queries by chaining
+ method calls together, leading to highly readable code. It also allows
+ you to take advantage of intellisense/code completion in your favourite
+ IDE.
+
+
+ Note that most fluent operations end with an execute()
+ statement which actually performs the invocation. You may also invoke
+ several configuration operations just prior to the execute() statement,
+ such as encodedJson() or encodedXml().
+
+
+
+
+
+
+ Searching for resources is probably the most common initial scenario for
+ client applications, so we'll start the demonstration there. The FHIR search
+ operation generally uses a URL with a set of predefined search parameters,
+ and returns a Bundle containing zero-or-more resources which matched the
+ given search criteria.
+
+
+ Search is a very powerful mechanism, with advanced features such as paging,
+ including linked resources, etc. See the FHIR
+ search specification
+ for more information.
+
+
+
+ Note on Bundle types: As of DSTU2, FHIR defines Bundle as a resource
+ instead of an Atom feed as it was in DSTU1. In code that was written for
+ DSTU1 you would typically use the ca.uhn.fhir.model.api.Bundle
+ class to represent a bundle, and that is that default return type for search
+ methods. If you are implemeting a DSTU2+ server, is recommended to use a
+ Bundle resource class instead (e.g. ca.uhn.fhir.model.dstu2.resource.Bundle
+ or org.hl7.fhir.instance.model.Bundle). Many of the examples below include
+ a chained invocation similar to
+ .returnBundle(ca.uhn.fhir.model.dstu2.resource.Bundle.class), which
+ instructs the search method which bundle type should be returned.
+
+
+
+ The following example shows how to query using the generic client:
+
+
+
+
+
+
+
Search - Multi-valued Parameters (ANY/OR)
+
+ To search for a set of possible values where ANY should be matched,
+ you can provide multiple values to a parameter, as shown in the example below.
+ This leads to a URL resembling ?family=Smith,Smyth
+
+
+
+
+
+
+
Search - Multi-valued Parameters (ALL/AND)
+
+ To search for a set of possible values where ALL should be matched,
+ you can provide multiple instances of a parameter, as shown in the example below.
+ This leads to a URL resembling ?address=Toronto&address=Ontario&address=Canada
+
+
+
+
+
+
+
Search - Paging
+
+ If the server supports paging results, the client has a page method
+ which can be used to load subsequent pages.
+
+
+
+
+
+
+
Search - Composite Parameters
+
+ If a composite parameter is being searched on, the parameter
+ takes a "left" and "right" operand, each of which is
+ a parameter from the resource being seached. The following example shows the
+ syntax.
+
+
+
+
+
+
+
Search - By plain URL
+
+ You can also perform a search using a String URL, instead
+ of using the fluent method calls to build the URL. This
+ can be useful if you have a URL you retrieved from
+ somewhere else that you want to use as a search.
+
+
+
+
+
+
+
Search - Other Query Options
+
+ The fluent search also has methods for sorting, limiting, specifying
+ JSON encoding, _include, _revinclude, _lastUpdated, _tag, etc.
+
+
+
+
+
+
+
Search - Using HTTP POST
+
+ The FHIR specification allows the use of an HTTP POST to transmit a search to a server instead of
+ using
+ an HTTP GET. With this style of search, the search parameters are included in the request body
+ instead
+ of the request URL, which can be useful if you need to transmit a search with a large number
+ of parameters.
+
+
+ The usingStyle() method controls which style to use. By default, GET style is used
+ unless the client detects that the request would result in a very long URL (over 8000 chars) in which
+ case the client automatically switches to POST.
+
+
+ An alternate form of the search URL (using a URL ending with_search) was also
+ supported in FHIR DSTU1. This form is no longer valid in FHIR DSTU2, but HAPI retains support
+ for using this form in order to interoperate with servers which use it.
+
+
+
+
+
+
+
Search - Compartments
+
+ To search a
+ resource compartment,
+ simply use the withIdAndCompartment
+ method in your search.
+
+
+
+
+
+
+
Search - Subsetting (_summary and _elements)
+
+ Sometimes you may want to only ask the server to include some parts of returned
+ resources (instead of the whole resource). Typically this is for performance or
+ optimization reasons, but there may also be privacy reasons for doing this.
+
+
+ To request that the server return only "summary" elements (those elements
+ defined in the specification with the "Σ" flag), you can use the
+ summaryMode(SummaryEnum) qualifier:
+
+
+
+
+
+
+ To request that the server return only elements from a custom list
+ provided by the client, you can use the elementsSubset(String...)
+ qualifier:
+
+
+
+
+
+
+
+
+
+
+ The following example shows how to perform a create
+ operation using the generic client:
+
+
+
+
+
+
+
Conditional Creates
+
+ FHIR also specifies a type of update called "conditional create", where
+ a set of search parameters are provided and a new resource is only
+ created if no existing resource matches those parameters. See the
+ FHIR specification for more information on conditional creation.
+
+
+
+
+
+
+
+
+
+ Given a resource name and ID, it is simple to retrieve
+ the latest version of that resource (a 'read')
+
+
+
+
+
+
+ By adding a version string, it is also possible to retrieve a
+ specific version (a 'vread')
+
+
+
+
+
+
+ It is also possible to retrieve a resource given its absolute
+ URL (this will override the base URL set on the client)
+
+
+
+
+
+
+
+ See also the page on
+ ETag Support
+ for information on specifying a matching version in the
+ client request.
+
+
+
+
+
+
+ The following example shows how to perform a delete
+ operation using the generic client:
+
+
+
+
+
+
Conditional Deletes
+
+ Conditional deletions are also possible, which is a form where
+ instead of deleting a resource using its logical ID, you specify
+ a set of search criteria and a single resource is deleted if
+ it matches that criteria. Note that this is not a mechanism
+ for bulk deletion; see the FHIR specification for information
+ on conditional deletes and how they are used.
+
+
+
+
+
+
+
+
+
+ Updating a resource is similar to creating one, except that
+ an ID must be supplied since you are updating a previously
+ existing resource instance.
+
+
+ The following example shows how to perform an update
+ operation using the generic client:
+
+
+
+
+
+
+
Conditional Updates
+
+ FHIR also specifies a type of update called "conditional updates", where
+ insetad of using the logical ID of a resource to update, a set of
+ search parameters is provided. If a single resource matches that set of
+ parameters, that resource is updated. See the FHIR specification for
+ information on how conditional updates work.
+
+
+
+
+
+
+
ETags and Resource Contention
+
+ See also the page on
+ ETag Support
+ for information on specifying a matching version in the
+ client request.
+
+
+
+
+
+
+ To retrieve the version history of all resources, or all resources of a given type, or
+ of a specific instance of a resource, you call the history()
+ method.
+
+
+
+
+
+
+
+ If you are using a DSTU2 compliant server, you should instead use the
+ Bundle resource which is found in the DSTU2 structures JAR, as shown
+ in the syntax below. Note that in both cases, the class name is Bundle,
+ but the DSTU2 bundle is found in the .resources. package.
+
+
+
+
+
+
+
+ You can also optionally request that only resource versions
+ later than a given date, and/or only up to a given count (number)
+ of resource versions be returned.
+
+
+
+
+
+
+
+
+
+ The following example shows how to execute a transaction using the generic client:
+
+
+
+
+
+
+
+
+
+ To retrieve the server's conformance statement, simply call the conformance()
+ method as shown below.
+
+
+
+
+
+
+
+
+
+
+ In the FHIR DSTU2 version, operations (referred to as "extended operations")
+ were added. These operations are an RPC style of invocation, with a set of
+ named input parameters passed to the server and a set of named output
+ parameters returned back.
+
+
+ To invoke an operation using the client, you simply need to create the
+ input
+ Parameters
+ resource, then pass that to the operation() fluent method.
+
+
+ The example below shows a simple operation call.
+
+
+
+
+
+
+
+ Note that if the operation does not require any input parameters,
+ you may also invoke the operation using the following form. Note that
+ the withNoParameters still requires you to provide the
+ type of the Parameters resource so that it can return the correct type in
+ the response.
+
+
+
+
+
+
+
+
+ By default, the client will invoke operations using the HTTP POST form.
+ The FHIR specification also allows requests to use the HTTP GET verb
+ if the operation is idempotent and has no composite/resource parameters.
+ Use the following form to invoke operation with HTTP GET.
+
+
+
+
+
+
+
+
+
+ The $validate operation asks the server to test a given resource
+ to see if it would be acceptable as a create/update on that server.
+ The client has built-in support for this operation.
+
+
+ If the client is in DSTU1 mode, the method below will invoke the
+ DSTU1 validation style instead.
+
+
+
+
+
+
+
+
+
+
+
+ The $process-message operation asks the server to accept a fhir
+ message bundle for processing.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This section contains ways of customizing the request sent by the client
+
+
+
+
+
+ The Cache-Control 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.
+
+
+
+ To add a cache control directive in a request:
+