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) 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" + -// "<id>d039f91a-cc3c-4013-988e-af4d8d0614bd</id>\n" + -// "<os:totalResults xmlns:os=\"http://a9.com/-/spec/opensearch/1.1/\">1</os:totalResults>\n" + -// "<author>\n" + -// "<name>ca.uhn.fhir.rest.server.DummyRestfulServer</name>\n" + -// "</author>\n" + -// "<entry>\n" + -// "<content type=\"text/xml\">" -// + "<Patient xmlns=\"http://hl7.org/fhir\">" -// + "<text><status value=\"generated\" /><div xmlns=\"http://www.w3.org/1999/xhtml\">John Cardinal: 444333333 </div></text>" -// + "<identifier><label value=\"SSN\" /><system value=\"http://orionhealth.com/mrn\" /><value value=\"PRP1660\" /></identifier>" -// + "<name><use value=\"official\" /><family value=\"Cardinal\" /><given value=\"John\" /></name>" -// + "<name><family value=\"Kramer\" /><given value=\"Doe\" /></name>" -// + "<telecom><system value=\"phone\" /><value value=\"555-555-2004\" /><use value=\"work\" /></telecom>" -// + "<gender><coding><system value=\"http://hl7.org/fhir/v3/AdministrativeGender\" /><code value=\"M\" /></coding></gender>" -// + "<address><use value=\"home\" /><line value=\"2222 Home Street\" /></address><active value=\"true\" />" -// + "</Patient>" -// + "</content>\n" -// + " </entry>\n" -// + "</feed>"; -// //@formatter:on - // return msg; - } - - private String getResourceResult() { - //@formatter:off - String msg = - "<Patient xmlns=\"http://hl7.org/fhir\">" - + "<text><status value=\"generated\" /><div xmlns=\"http://www.w3.org/1999/xhtml\">John Cardinal: 444333333 </div></text>" - + "<identifier><label value=\"SSN\" /><system value=\"http://orionhealth.com/mrn\" /><value value=\"PRP1660\" /></identifier>" - + "<name><use value=\"official\" /><family value=\"Cardinal\" /><given value=\"John\" /></name>" - + "<name><family value=\"Kramer\" /><given value=\"Doe\" /></name>" - + "<telecom><system value=\"phone\" /><value value=\"555-555-2004\" /><use value=\"work\" /></telecom>" - + "<gender><coding><system value=\"http://hl7.org/fhir/v3/AdministrativeGender\" /><code value=\"M\" /></coding></gender>" - + "<address><use value=\"home\" /><line value=\"2222 Home Street\" /></address><active value=\"true\" />" - + "</Patient>"; - //@formatter:on - return msg; - } - - @Test - public void testCreatePopulatesIsCreated() throws Exception { - - Patient p1 = createPatientP1(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); - 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(""), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); - MethodOutcome resp = client.create().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).execute(); - assertTrue(resp.getCreated()); - - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - resp = client.create().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).execute(); - assertNull(resp.getCreated()); - - ourLog.info("lastRequest: {}", ((GenericClient) client).getLastRequest()); - ourLog.info("lastResponse: {}", ((GenericClient) client).getLastResponse()); - 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 { - - Patient p1 = createPatientP1(); - - 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), 201, "OK")); - when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); - 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(""), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - int count = 0; - client.create().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).execute(); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - assertThat(extractBody(capt, count), containsString("value=\"John\"")); - count++; - - String resourceAsString = ourCtx.newJsonParser().encodeResourceToString(p1); - client - .create() - .resource(resourceAsString) - .execute(); - - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.JSON.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - assertThat(extractBody(capt, count), containsString("[\"John\"]")); - count++; - - /* - * e.g. Now try with reversed encoding (provide a string that's in JSON and ask the client to use XML) - */ - - client.create().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).encodedJson().execute(); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.JSON.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - assertThat(extractBody(capt, count), containsString("[\"John\"]")); - count++; - - client.create().resource(ourCtx.newJsonParser().encodeResourceToString(p1)).encodedXml().execute(); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - assertThat(extractBody(capt, count), containsString("value=\"John\"")); - count++; - - } - - @Test - public void testCreateWithTag() throws Exception { - - Patient p1 = createPatientP1(); - p1.getMeta().addTag("http://hl7.org/fhir/tag", "urn:happytag", "This is a happy resource"); - - 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), 201, "OK")); - when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); - 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(""), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - MethodOutcome outcome = client.create().resource(p1).execute(); - assertEquals("44", outcome.getId().getIdPart()); - assertEquals("22", outcome.getId().getVersionIdPart()); - - int count = 0; - - assertEquals("http://example.com/fhir/Patient", capt.getValue().getURI().toString()); - assertEquals("POST", capt.getValue().getMethod()); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - count++; - - /* - * Try fluent options - */ - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); - client.create().resource(p1).execute(); - assertEquals("http://example.com/fhir/Patient", capt.getAllValues().get(1).getURI().toString()); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - count++; - - String resourceText = "<Patient xmlns=\"http://hl7.org/fhir\"> </Patient>"; - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); - client.create().resource(resourceText).execute(); - assertEquals("http://example.com/fhir/Patient", capt.getAllValues().get(2).getURI().toString()); - assertEquals(resourceText, IOUtils.toString(((HttpPost) capt.getAllValues().get(2)).getEntity().getContent())); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - count++; - } - - @Test - public void testCreateWithTagNonFluent() throws Exception { - - Patient p1 = createPatientP1(); - p1.getMeta().addTag("http://hl7.org/fhir/tag", "urn:happytag", "This is a happy resource"); - - 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), 201, "OK")); - when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); - 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(""), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - MethodOutcome outcome = client.create().resource(p1).execute(); - assertEquals("44", outcome.getId().getIdPart()); - assertEquals("22", outcome.getId().getVersionIdPart()); - - assertEquals("http://example.com/fhir/Patient", capt.getValue().getURI().toString()); - assertEquals("POST", capt.getValue().getMethod()); - Header catH = capt.getValue().getFirstHeader("Category"); - assertNull(catH); - } - - /** - * Test for issue #60 - */ - @Test - public void testCreateWithUtf8Characters() throws Exception { - String name = "測試醫院"; - Organization org = new Organization(); - org.setName(name); - org.addIdentifier().setSystem("urn:system").setValue("testCreateWithUtf8Characters_01"); - - 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), 201, "OK")); - when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); - 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(""), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - int count = 0; - client.create().resource(org).prettyPrint().encodedXml().execute(); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - assertThat(extractBody(capt, count), containsString("<name value=\"測試醫院\"/>")); - count++; - - } - - @Test - public void testDelete() throws Exception { - OperationOutcome oo = new OperationOutcome(); - oo.addIssue().addLocation("testDelete01"); - String ooStr = ourCtx.newXmlParser().encodeResourceToString(oo); - - 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), 201, "OK")); - when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); - 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(ooStr), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - OperationOutcome outcome = (OperationOutcome) client.delete().resourceById("Patient", "123").execute(); - - assertEquals("http://example.com/fhir/Patient/123", capt.getValue().getURI().toString()); - assertEquals("DELETE", capt.getValue().getMethod()); - assertEquals("testDelete01", outcome.getIssueFirstRep().getLocation().get(0).getValue()); - - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader("LKJHLKJGLKJKLL"), Charset.forName("UTF-8"))); - outcome = (OperationOutcome) client.delete().resourceById(new IdType("Location", "123", "456")).prettyPrint().encodedJson().execute(); - - assertEquals("http://example.com/fhir/Location/123?_pretty=true", capt.getAllValues().get(1).getURI().toString()); - assertEquals("DELETE", capt.getValue().getMethod()); - assertEquals(null, outcome); - - } - - @Test - public void testHistory() 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; - - response = client - .history() - .onServer() - .andReturnBundle(Bundle.class) - .execute(); - assertEquals("http://example.com/fhir/_history", capt.getAllValues().get(idx).getURI().toString()); - assertEquals(1, response.getEntry().size()); - idx++; - - response = client - .history() - .onType(Patient.class) - .andReturnBundle(Bundle.class) - .execute(); - assertEquals("http://example.com/fhir/Patient/_history", capt.getAllValues().get(idx).getURI().toString()); - assertEquals(1, response.getEntry().size()); - idx++; - - response = client - .history() - .onInstance(new IdType("Patient", "123")) - .andReturnBundle(Bundle.class) - .execute(); - assertEquals("http://example.com/fhir/Patient/123/_history", capt.getAllValues().get(idx).getURI().toString()); - assertEquals(1, response.getEntry().size()); - idx++; - } - - @Test - public void testMissing() throws Exception { - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); - 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(getPatientFeedWithOneResult()), Charset.forName("UTF-8"))); - } - }); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); - - client.search().forResource("Patient").where(Patient.NAME.isMissing(true)).returnBundle(Bundle.class).execute(); - assertEquals("http://example.com/fhir/Patient?name%3Amissing=true", capt.getValue().getRequestLine().getUri()); - - client.search().forResource("Patient").where(Patient.NAME.isMissing(false)).returnBundle(Bundle.class).execute(); - assertEquals("http://example.com/fhir/Patient?name%3Amissing=false", capt.getValue().getRequestLine().getUri()); - } - - @Test - public void testRead() throws Exception { - - String msg = getResourceResult(); - - 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"))); - Header[] headers = new Header[] { - new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 GMT"), - new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333"), - }; - when(myHttpResponse.getAllHeaders()).thenReturn(headers); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Patient response = client - .read() - .resource(Patient.class) - .withId(new IdType("Patient/1234")) - .execute(); - - assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); - - assertEquals("http://foo.com/Patient/123/_history/2333", response.getIdElement().getValue()); - - InstantType lm = response.getMeta().getLastUpdatedElement(); - lm.setTimeZoneZulu(true); - assertEquals("1995-11-15T04:58:08.000Z", lm.getValueAsString()); - - } - - @Test - public void testReadFluent() throws Exception { - - String msg = getResourceResult(); - - 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"))); - Header[] headers = new Header[] { - new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 GMT"), - new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333"), - }; - when(myHttpResponse.getAllHeaders()).thenReturn(headers); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - int count = 0; - - Patient response = client.read().resource(Patient.class).withId(new IdType("Patient/1234")).execute(); - assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); - assertEquals("http://example.com/fhir/Patient/1234", capt.getAllValues().get(count++).getURI().toString()); - - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - response = (Patient) client.read().resource("Patient").withId("1234").execute(); - assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); - assertEquals("http://example.com/fhir/Patient/1234", capt.getAllValues().get(count++).getURI().toString()); - - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - response = (Patient) client.read().resource("Patient").withId(567L).execute(); - assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); - assertEquals("http://example.com/fhir/Patient/567", capt.getAllValues().get(count++).getURI().toString()); - - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - response = client.read().resource(Patient.class).withIdAndVersion("1234", "22").execute(); - assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); - assertEquals("http://example.com/fhir/Patient/1234/_history/22", capt.getAllValues().get(count++).getURI().toString()); - - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - response = client.read().resource(Patient.class).withUrl("http://foo/Patient/22").execute(); - assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); - assertEquals("http://foo/Patient/22", capt.getAllValues().get(count++).getURI().toString()); - - } - - @Test - public void testReadWithAbsoluteUrl() throws Exception { - - String msg = getResourceResult(); - - 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"))); - Header[] headers = new Header[] { new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 GMT"), - new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333"), - }; - when(myHttpResponse.getAllHeaders()).thenReturn(headers); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Patient response = client - .read() - .resource(Patient.class) - .withUrl(new IdType("http://somebase.com/path/to/base/Patient/1234")) - .execute(); - assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); - assertEquals("http://somebase.com/path/to/base/Patient/1234", capt.getAllValues().get(0).getURI().toString()); - - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - response = client - .read() - .resource(Patient.class) - .withUrl(new IdType("http://somebase.com/path/to/base/Patient/1234/_history/222")) - .execute(); - assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); - assertEquals("http://somebase.com/path/to/base/Patient/1234/_history/222", capt.getAllValues().get(1).getURI().toString()); - - } - - @SuppressWarnings("unused") - @Test - public void testSearchAllResources() throws Exception { - - 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.search() - .forAllResources() - .where(Patient.NAME.matches().value("james")) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/?name=james", capt.getValue().getURI().toString()); - - } - - @SuppressWarnings("unused") - @Test - public void testSearchAutomaticallyUsesPost() throws Exception { - - 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - String longValue = StringUtils.leftPad("", 20000, 'B'); - - Bundle response = client.search() - .forResource("Patient") - .where(Patient.NAME.matches().value(longValue)) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient/_search", capt.getValue().getURI().toString()); - - HttpEntityEnclosingRequestBase enc = (HttpEntityEnclosingRequestBase) capt.getValue(); - UrlEncodedFormEntity ent = (UrlEncodedFormEntity) enc.getEntity(); - String string = IOUtils.toString(ent.getContent()); - ourLog.info(string); - 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 { - - 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"); - Bundle response = client - .search() - .forResource(Patient.class) - .withIdAndCompartment("123", "fooCompartment") - .where(Patient.BIRTHDATE.afterOrEquals().day("2011-01-02")) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://foo/Patient/123/fooCompartment?birthdate=ge2011-01-02", capt.getValue().getURI().toString()); - - ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(response)); - - assertEquals("PRP1660", BundleUtil.toListOfResourcesOfType(ourCtx, response, Patient.class).get(0).getIdentifier().get(0).getValue()); - - try { - client - .search() - .forResource(Patient.class) - .withIdAndCompartment("", "fooCompartment") - .where(Patient.BIRTHDATE.afterOrEquals().day("2011-01-02")) - .returnBundle(Bundle.class) - .execute(); - fail(); - } catch (InvalidRequestException e) { - assertThat(e.toString(), containsString("null or empty for compartment")); - } - - } - - @SuppressWarnings("unused") - @Test - public void testSearchByComposite() throws Exception { - - 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://foo"); - - Bundle response = client.search() - .forResource("Observation") - .where(Observation.CODE_VALUE_DATE - .withLeft(Observation.CODE.exactly().code("FOO$BAR")) - .withRight(Observation.VALUE_DATE.exactly().day("2001-01-01"))) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://foo/Observation?" + Observation.SP_CODE_VALUE_DATE + "=" + URLEncoder.encode("FOO\\$BAR$2001-01-01", "UTF-8"), capt.getValue().getURI().toString()); - - } - - @SuppressWarnings("unused") - @Test - public void testSearchByDate() 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; - - @SuppressWarnings("deprecation") - Bundle response = client.search() - .forResource(Patient.class) - .encodedJson() - .where(Patient.BIRTHDATE.beforeOrEquals().day("2012-01-22")) - .and(Patient.BIRTHDATE.after().day("2011-01-01")) - .include(Patient.INCLUDE_ORGANIZATION) - .sort().ascending(Patient.BIRTHDATE) - .sort().descending(Patient.NAME) - .sort().defaultOrder(Patient.ADDRESS) - .count(123) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient?birthdate=le2012-01-22&birthdate=gt2011-01-01&_include=Patient%3Aorganization&_sort=birthdate%2C-name%2Caddress&_count=123&_format=json", - capt.getAllValues().get(idx++).getURI().toString()); - - response = client.search() - .forResource(Patient.class) - .encodedJson() - .where(Patient.BIRTHDATE.beforeOrEquals().day("2012-01-22")) - .and(Patient.BIRTHDATE.after().day("2011-01-01")) - .include(Patient.INCLUDE_ORGANIZATION) - .sort().ascending(Patient.BIRTHDATE) - .sort().descending(Patient.NAME) - .sort().defaultOrder(Patient.ADDRESS) - .count(123) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient?birthdate=le2012-01-22&birthdate=gt2011-01-01&_include=Patient%3Aorganization&_sort=birthdate%2C-name%2Caddress&_count=123&_format=json", - capt.getAllValues().get(idx++).getURI().toString()); - - response = client.search() - .forResource(Patient.class) - .encodedJson() - .where(Patient.BIRTHDATE.beforeOrEquals().day("2012-01-22").orAfter().day("2020-01-01")) - .and(Patient.BIRTHDATE.after().day("2011-01-01")) - .returnBundle(Bundle.class) - .execute(); - - String comma = "%2C"; - assertEquals("http://example.com/fhir/Patient?birthdate=le2012-01-22" + comma + "gt2020-01-01&birthdate=gt2011-01-01&_format=json", capt.getAllValues().get(idx++).getURI().toString()); - } - - @SuppressWarnings("unused") - @Test - public void testSearchByNumberExact() 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) - .where(Observation.VALUE_QUANTITY.greaterThan().number(123).andUnits("foo", "bar")) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Observation?value-quantity=gt123%7Cfoo%7Cbar", capt.getValue().getURI().toString()); - } - - @SuppressWarnings("unused") - @Test - public void testSearchByProfile() throws Exception { - - 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.search() - .forResource(Patient.class) - .withProfile("http://1") - .withProfile("http://2") - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient?_profile=http%3A%2F%2F1&_profile=http%3A%2F%2F2", capt.getValue().getURI().toString()); - - } - - @SuppressWarnings("unused") - @Test - public void testSearchByQuantity() throws Exception { - - 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.search() - .forResource(Patient.class) - .where(Encounter.LENGTH.exactly().number(123)) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient?length=123", capt.getValue().getURI().toString()); - - } - - @SuppressWarnings("unused") - @Test - public void testSearchByReferenceProperty() throws Exception { - - 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.search() - .forResource(Patient.class) - .where(Patient.GENERAL_PRACTITIONER.hasChainedProperty(Organization.NAME.matches().value("ORG0"))) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient?general-practitioner.name=ORG0", capt.getValue().getURI().toString()); - - } - - @SuppressWarnings("unused") - @Test - public void testSearchByReferenceSimple() throws Exception { - - 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.search() - .forResource("Patient") - .where(Patient.GENERAL_PRACTITIONER.hasId("123")) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient?general-practitioner=123", capt.getValue().getURI().toString()); - - } - - @SuppressWarnings("unused") - @Test - public void testSearchBySecurity() throws Exception { - - 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.search() - .forResource(Patient.class) - .withSecurity("urn:foo", "123") - .withSecurity("urn:bar", "456") - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient?_security=urn%3Afoo%7C123&_security=urn%3Abar%7C456", capt.getValue().getURI().toString()); - - } - - @SuppressWarnings("unused") - @Test - public void testSearchByString() throws Exception { - - 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.search() - .forResource("Patient") - .where(Patient.NAME.matches().value("james")) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient?name=james", capt.getValue().getURI().toString()); - - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - response = client.search() - .forResource("Patient") - .where(Patient.NAME.matches().values("AAA", "BBB", "C,C")) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient?name=" + URLEncoder.encode("AAA,BBB,C\\,C", "UTF-8"), capt.getAllValues().get(1).getURI().toString()); - - } - - @SuppressWarnings("unused") - @Test - public void testSearchByStringExact() throws Exception { - - 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.search() - .forResource("Patient") - .where(Patient.NAME.matchesExactly().value("james")) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient?name%3Aexact=james", capt.getValue().getURI().toString()); - - } - - @SuppressWarnings("unused") - @Test - public void testSearchByTag() throws Exception { - - 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.search() - .forResource(Patient.class) - .withTag("urn:foo", "123") - .withTag("urn:bar", "456") - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient?_tag=urn%3Afoo%7C123&_tag=urn%3Abar%7C456", capt.getValue().getURI().toString()); - - } - - @SuppressWarnings("unused") - @Test - public void testSearchByToken() throws Exception { - - 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.search() - .forResource("Patient") - .where(Patient.IDENTIFIER.exactly().systemAndCode("http://example.com/fhir", "ZZZ")) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient?identifier=http%3A%2F%2Fexample.com%2Ffhir%7CZZZ", capt.getValue().getURI().toString()); - - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - response = client.search() - .forResource("Patient") - .where(Patient.IDENTIFIER.exactly().code("ZZZ")) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient?identifier=ZZZ", capt.getAllValues().get(1).getURI().toString()); - - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - response = client.search() - .forResource("Patient") - .where(Patient.IDENTIFIER.exactly().codings(new Coding("A", "B", "ZZZ"), new Coding("C", "D", "ZZZ"))) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient?identifier=" + URLEncoder.encode("A|B,C|D", "UTF-8"), capt.getAllValues().get(2).getURI().toString()); - - } - - @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 - */ - @SuppressWarnings("unused") - @Test - public void testSearchByTokenWithEscaping() 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://foo"); - int index = 0; - String wantPrefix = "http://foo/Patient?identifier="; - - Bundle response = client.search() - .forResource("Patient") - .where(Patient.IDENTIFIER.exactly().systemAndCode("1", "2")) - .returnBundle(Bundle.class) - .execute(); - String wantValue = "1|2"; - String url = capt.getAllValues().get(index).getURI().toString(); - assertThat(url, Matchers.startsWith(wantPrefix)); - assertEquals(wantValue, UrlUtil.unescape(url.substring(wantPrefix.length()))); - assertEquals(UrlUtil.escape(wantValue), url.substring(wantPrefix.length())); - index++; - - response = client.search() - .forResource("Patient") - .where(Patient.IDENTIFIER.exactly().systemAndCode("1,2", "3,4")) - .returnBundle(Bundle.class) - .execute(); - wantValue = "1\\,2|3\\,4"; - url = capt.getAllValues().get(index).getURI().toString(); - assertThat(url, Matchers.startsWith(wantPrefix)); - assertEquals(wantValue, UrlUtil.unescape(url.substring(wantPrefix.length()))); - assertEquals(UrlUtil.escape(wantValue), url.substring(wantPrefix.length())); - index++; - } - - @SuppressWarnings("unused") - @Test - public void testSearchIncludeRecursive() throws Exception { - - 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.search() - .forResource(Patient.class) - .include(Patient.INCLUDE_ORGANIZATION) - .include(Patient.INCLUDE_LINK.asRecursive()) - .include(Patient.INCLUDE_ALL.asNonRecursive()) - .returnBundle(Bundle.class) - .execute(); - - assertThat(capt.getValue().getURI().toString(), containsString("http://example.com/fhir/Patient?")); - assertThat(capt.getValue().getURI().toString(), containsString("_include=" + UrlUtil.escape(Patient.INCLUDE_ORGANIZATION.getValue()))); - assertThat(capt.getValue().getURI().toString(), containsString("_include%3Arecurse=" + UrlUtil.escape(Patient.INCLUDE_LINK.getValue()))); - assertThat(capt.getValue().getURI().toString(), containsString("_include=*")); - - } - - @SuppressWarnings("unused") - @Test - public void testSearchUsingGetSearch() throws Exception { - - 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.search() - .forResource("Patient") - .where(Patient.NAME.matches().value("james")) - .usingStyle(SearchStyleEnum.GET_WITH_SEARCH) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient/_search?name=james", capt.getValue().getURI().toString()); - } - - @SuppressWarnings("unused") - @Test - public void testSearchUsingPost() throws Exception { - - 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.search() - .forResource("Patient") - .where(Patient.NAME.matches().value("james")) - .usingStyle(SearchStyleEnum.POST) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient/_search", capt.getValue().getURI().toString()); - - HttpEntityEnclosingRequestBase enc = (HttpEntityEnclosingRequestBase) capt.getValue(); - UrlEncodedFormEntity ent = (UrlEncodedFormEntity) enc.getEntity(); - String string = IOUtils.toString(ent.getContent()); - ourLog.info(string); - assertEquals("name=james", string); - } - - @Test - public void testSearchWithAbsoluteUrl() throws Exception { - - 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client - .search() - .byUrl("http://example.com/fhir/Patient?birthdate=le2012-01-22&birthdate=gt2011-01-01&_include=Patient%3Aorganization&_sort%3Aasc=birthdate&_sort%3Adesc=name&_count=123&_format=json") - .returnBundle(Bundle.class) - .execute(); - - assertEquals(1, response.getEntry().size()); - } - - @SuppressWarnings("unused") - @Test - public void testSearchWithClientEncodingAndPrettyPrintConfig() throws Exception { - - 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - client.setPrettyPrint(true); - client.setEncoding(EncodingEnum.JSON); - - Bundle response = client.search() - .forResource(Patient.class) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient?_format=json&_pretty=true", capt.getValue().getURI().toString()); - - } - - @SuppressWarnings("unused") - @Test - public void testSearchWithEscapedParameters() throws Exception { - - 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.search() - .forResource("Patient") - .where(Patient.NAME.matches().values("NE,NE", "NE,NE")) - .where(Patient.NAME.matchesExactly().values("E$E")) - .where(Patient.NAME.matches().values("NE\\NE")) - .where(Patient.NAME.matchesExactly().values("E|E")) - .returnBundle(Bundle.class) - .execute(); - - assertThat(capt.getValue().getURI().toString(), containsString("%3A")); - assertEquals("http://example.com/fhir/Patient?name=NE\\,NE,NE\\,NE&name=NE\\\\NE&name:exact=E\\$E&name:exact=E\\|E", UrlUtil.unescape(capt.getValue().getURI().toString())); - } - - @SuppressWarnings("unused") - @Test - public void testSearchWithInternalServerError() throws Exception { - - 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), 500, "INTERNAL ERRORS")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader("Server Issues!"), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - try { - client - .search() - .forResource(Patient.class) - .returnBundle(Bundle.class) - .execute(); - fail(); - } catch (InternalErrorException e) { - assertEquals(e.getMessage(), "HTTP 500 INTERNAL ERRORS: Server Issues!"); - assertEquals(e.getResponseBody(), "Server Issues!"); - } - - } - - @SuppressWarnings("unused") - @Test - public void testSearchWithNonFhirResponse() throws Exception { - - 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_TEXT + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader("Server Issues!"), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - try { - client.search().forResource(Patient.class).returnBundle(Bundle.class).execute(); - fail(); - } catch (NonFhirResponseException e) { - assertThat(e.getMessage(), StringContains.containsString("Server Issues!")); - } - - } - - @SuppressWarnings("unused") - @Test - public void testSearchWithReverseInclude() throws Exception { - - 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.search() - .forResource(Patient.class) - .encodedJson() - .revInclude(Provenance.INCLUDE_TARGET) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient?_revinclude=Provenance%3Atarget&_format=json", capt.getValue().getURI().toString()); - - } - - @Test - public void testSetDefaultEncoding() throws Exception { - - String msg = ourCtx.newJsonParser().encodeResourceToString(new Patient()); - - 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_JSON + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - // Header[] headers = new Header[] { new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 - // GMT"), - // new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333"), - // new BasicHeader(Constants.HEADER_CATEGORY, "http://foo/tagdefinition.html; scheme=\"http://hl7.org/fhir/tag\"; - // label=\"Some tag\"") }; - // when(myHttpResponse.getAllHeaders()).thenReturn(headers); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - (client).setEncoding(EncodingEnum.JSON); - int count = 0; - - client - .read() - .resource(Patient.class) - .withId(new IdType("Patient/1234")) - .execute(); - assertEquals("http://example.com/fhir/Patient/1234?_format=json", capt.getAllValues().get(count).getURI().toString()); - count++; - - } - - @Test - public void testTransaction() throws Exception { - Bundle input = createTransactionBundleInput(); - Bundle output = createTransactionBundleOutput(); - - String msg = ourCtx.newJsonParser().encodeResourceToString(output); - - 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_JSON + "; 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.transaction() - .withBundle(input) - .execute(); - - assertEquals("http://example.com/fhir", capt.getValue().getURI().toString()); - assertEquals(input.getEntry().get(0).getResource().getId(), response.getEntry().get(0).getResource().getId()); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(0).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - - } - - @Test - public void testTransactionXml() throws Exception { - Bundle input = createTransactionBundleInput(); - Bundle output = createTransactionBundleOutput(); - - String msg = ourCtx.newXmlParser().encodeResourceToString(output); - - 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.transaction() - .withBundle(input) - .execute(); - - assertEquals("http://example.com/fhir", capt.getValue().getURI().toString()); - assertEquals(input.getEntry().get(0).getResource().getId(), response.getEntry().get(0).getResource().getId()); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(0).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - - } - - 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 { - - Patient p1 = new Patient(); - p1.addIdentifier().setSystem("foo:bar").setValue("12345"); - p1.addName().setFamily("Smith").addGiven("John"); - - 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), 201, "OK")); - when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); - 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(""), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - try { - client.update().resource(p1).execute(); - fail(); - } catch (InvalidRequestException e) { - // should happen because no ID set - } - - assertEquals(0, capt.getAllValues().size()); - - p1.setId("44"); - client.update().resource(p1).execute(); - - int count = 0; - - assertEquals(1, capt.getAllValues().size()); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - count++; - - MethodOutcome outcome = client.update().resource(p1).execute(); - assertEquals("44", outcome.getId().getIdPart()); - assertEquals("22", outcome.getId().getVersionIdPart()); - - assertEquals(2, capt.getAllValues().size()); - - assertEquals("http://example.com/fhir/Patient/44", capt.getValue().getURI().toString()); - assertEquals("PUT", capt.getValue().getMethod()); - - /* - * Try fluent options - */ - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); - client.update().resource(p1).withId("123").execute(); - assertEquals(3, capt.getAllValues().size()); - assertEquals("http://example.com/fhir/Patient/123", capt.getAllValues().get(2).getURI().toString()); - - String resourceText = "<Patient xmlns=\"http://hl7.org/fhir\"> </Patient>"; - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); - client.update().resource(resourceText).withId("123").execute(); - assertEquals("http://example.com/fhir/Patient/123", capt.getAllValues().get(3).getURI().toString()); - assertEquals(resourceText, IOUtils.toString(((HttpPut) capt.getAllValues().get(3)).getEntity().getContent())); - assertEquals(4, capt.getAllValues().size()); - - } - - @Test - public void testUpdateWithStringAutoDetectsEncoding() throws Exception { - - Patient p1 = new Patient(); - p1.addIdentifier().setSystem("foo:bar").setValue("12345"); - p1.addName().setFamily("Smith").addGiven("John"); - - 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), 201, "OK")); - when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); - 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(""), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - int count = 0; - client.update().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).withId("1").execute(); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - assertThat(extractBody(capt, count), containsString("value=\"John\"")); - count++; - - client.update().resource(ourCtx.newJsonParser().encodeResourceToString(p1)).withId("1").execute(); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.JSON.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - assertThat(extractBody(capt, count), containsString("[\"John\"]")); - count++; - - /* - * e.g. Now try with reversed encoding (provide a string that's in JSON and ask the client to use XML) - */ - - client.update().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).withId("1").encodedJson().execute(); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.JSON.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - assertThat(extractBody(capt, count), containsString("[\"John\"]")); - count++; - - client.update().resource(ourCtx.newJsonParser().encodeResourceToString(p1)).withId("1").encodedXml().execute(); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - assertThat(extractBody(capt, count), containsString("value=\"John\"")); - 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 { - - String msg = getResourceResult(); - - 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"))); - Header[] headers = new Header[] { - new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 GMT"), - new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333"), - }; - when(myHttpResponse.getAllHeaders()).thenReturn(headers); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Patient response = client - .read() - .resource(Patient.class) - .withUrl("http://somebase.com/path/to/base/Patient/1234/_history/2222") - .execute(); - - assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); - assertEquals("http://somebase.com/path/to/base/Patient/1234/_history/2222", capt.getAllValues().get(0).getURI().toString()); - - } - - @BeforeClass - public static void beforeClass() { - ourCtx = FhirContext.forR4(); - } - - @AfterClass - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); - } - -} +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 final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(GenericClientTest.class); + private static FhirContext ourCtx; + 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 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; + } + + private String getPatientFeedWithOneResult() { + return ClientR4Test.getPatientFeedWithOneResult(ourCtx); +// //@formatter:off +// String msg = "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n" + +// "<title/>\n" + +// "<id>d039f91a-cc3c-4013-988e-af4d8d0614bd</id>\n" + +// "<os:totalResults xmlns:os=\"http://a9.com/-/spec/opensearch/1.1/\">1</os:totalResults>\n" + +// "<author>\n" + +// "<name>ca.uhn.fhir.rest.server.DummyRestfulServer</name>\n" + +// "</author>\n" + +// "<entry>\n" + +// "<content type=\"text/xml\">" +// + "<Patient xmlns=\"http://hl7.org/fhir\">" +// + "<text><status value=\"generated\" /><div xmlns=\"http://www.w3.org/1999/xhtml\">John Cardinal: 444333333 </div></text>" +// + "<identifier><label value=\"SSN\" /><system value=\"http://orionhealth.com/mrn\" /><value value=\"PRP1660\" /></identifier>" +// + "<name><use value=\"official\" /><family value=\"Cardinal\" /><given value=\"John\" /></name>" +// + "<name><family value=\"Kramer\" /><given value=\"Doe\" /></name>" +// + "<telecom><system value=\"phone\" /><value value=\"555-555-2004\" /><use value=\"work\" /></telecom>" +// + "<gender><coding><system value=\"http://hl7.org/fhir/v3/AdministrativeGender\" /><code value=\"M\" /></coding></gender>" +// + "<address><use value=\"home\" /><line value=\"2222 Home Street\" /></address><active value=\"true\" />" +// + "</Patient>" +// + "</content>\n" +// + " </entry>\n" +// + "</feed>"; +// //@formatter:on + // return msg; + } + + private String getResourceResult() { + //@formatter:off + String msg = + "<Patient xmlns=\"http://hl7.org/fhir\">" + + "<text><status value=\"generated\" /><div xmlns=\"http://www.w3.org/1999/xhtml\">John Cardinal: 444333333 </div></text>" + + "<identifier><label value=\"SSN\" /><system value=\"http://orionhealth.com/mrn\" /><value value=\"PRP1660\" /></identifier>" + + "<name><use value=\"official\" /><family value=\"Cardinal\" /><given value=\"John\" /></name>" + + "<name><family value=\"Kramer\" /><given value=\"Doe\" /></name>" + + "<telecom><system value=\"phone\" /><value value=\"555-555-2004\" /><use value=\"work\" /></telecom>" + + "<gender><coding><system value=\"http://hl7.org/fhir/v3/AdministrativeGender\" /><code value=\"M\" /></coding></gender>" + + "<address><use value=\"home\" /><line value=\"2222 Home Street\" /></address><active value=\"true\" />" + + "</Patient>"; + //@formatter:on + 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 { + + Patient p1 = createPatientP1(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); + 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(""), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); + MethodOutcome resp = client.create().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).execute(); + assertTrue(resp.getCreated()); + + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + resp = client.create().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).execute(); + assertNull(resp.getCreated()); + + ourLog.info("lastRequest: {}", ((GenericClient) client).getLastRequest()); + ourLog.info("lastResponse: {}", ((GenericClient) client).getLastResponse()); + ourLog.info("lastResponseBody: {}", ((GenericClient) client).getLastResponseBody()); + } + + @Test + public void testCreateWithStringAutoDetectsEncoding() throws Exception { + + Patient p1 = createPatientP1(); + + 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), 201, "OK")); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); + 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(""), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + int count = 0; + client.create().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).execute(); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + assertThat(extractBody(capt, count), containsString("value=\"John\"")); + count++; + + String resourceAsString = ourCtx.newJsonParser().encodeResourceToString(p1); + client + .create() + .resource(resourceAsString) + .execute(); + + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.JSON.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + assertThat(extractBody(capt, count), containsString("[\"John\"]")); + count++; + + /* + * e.g. Now try with reversed encoding (provide a string that's in JSON and ask the client to use XML) + */ + + client.create().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).encodedJson().execute(); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.JSON.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + assertThat(extractBody(capt, count), containsString("[\"John\"]")); + count++; + + client.create().resource(ourCtx.newJsonParser().encodeResourceToString(p1)).encodedXml().execute(); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + assertThat(extractBody(capt, count), containsString("value=\"John\"")); + count++; + + } + + @Test + public void testCreateWithTag() throws Exception { + + Patient p1 = createPatientP1(); + p1.getMeta().addTag("http://hl7.org/fhir/tag", "urn:happytag", "This is a happy resource"); + + 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), 201, "OK")); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); + 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(""), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + MethodOutcome outcome = client.create().resource(p1).execute(); + assertEquals("44", outcome.getId().getIdPart()); + assertEquals("22", outcome.getId().getVersionIdPart()); + + int count = 0; + + assertEquals("http://example.com/fhir/Patient", capt.getValue().getURI().toString()); + assertEquals("POST", capt.getValue().getMethod()); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + count++; + + /* + * Try fluent options + */ + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + client.create().resource(p1).execute(); + assertEquals("http://example.com/fhir/Patient", capt.getAllValues().get(1).getURI().toString()); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + count++; + + String resourceText = "<Patient xmlns=\"http://hl7.org/fhir\"> </Patient>"; + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + client.create().resource(resourceText).execute(); + assertEquals("http://example.com/fhir/Patient", capt.getAllValues().get(2).getURI().toString()); + assertEquals(resourceText, IOUtils.toString(((HttpPost) capt.getAllValues().get(2)).getEntity().getContent())); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + count++; + } + + @Test + public void testCreateWithTagNonFluent() throws Exception { + + Patient p1 = createPatientP1(); + p1.getMeta().addTag("http://hl7.org/fhir/tag", "urn:happytag", "This is a happy resource"); + + 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), 201, "OK")); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); + 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(""), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + MethodOutcome outcome = client.create().resource(p1).execute(); + assertEquals("44", outcome.getId().getIdPart()); + assertEquals("22", outcome.getId().getVersionIdPart()); + + assertEquals("http://example.com/fhir/Patient", capt.getValue().getURI().toString()); + assertEquals("POST", capt.getValue().getMethod()); + Header catH = capt.getValue().getFirstHeader("Category"); + assertNull(catH); + } + + /** + * Test for issue #60 + */ + @Test + public void testCreateWithUtf8Characters() throws Exception { + String name = "測試醫院"; + Organization org = new Organization(); + org.setName(name); + org.addIdentifier().setSystem("urn:system").setValue("testCreateWithUtf8Characters_01"); + + 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), 201, "OK")); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); + 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(""), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + int count = 0; + client.create().resource(org).prettyPrint().encodedXml().execute(); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + assertThat(extractBody(capt, count), containsString("<name value=\"測試醫院\"/>")); + count++; + + } + + @Test + public void testDelete() throws Exception { + OperationOutcome oo = new OperationOutcome(); + oo.addIssue().addLocation("testDelete01"); + String ooStr = ourCtx.newXmlParser().encodeResourceToString(oo); + + 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), 201, "OK")); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); + 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(ooStr), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + OperationOutcome outcome = (OperationOutcome) client.delete().resourceById("Patient", "123").execute(); + + assertEquals("http://example.com/fhir/Patient/123", capt.getValue().getURI().toString()); + assertEquals("DELETE", capt.getValue().getMethod()); + assertEquals("testDelete01", outcome.getIssueFirstRep().getLocation().get(0).getValue()); + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader("LKJHLKJGLKJKLL"), Charset.forName("UTF-8"))); + outcome = (OperationOutcome) client.delete().resourceById(new IdType("Location", "123", "456")).prettyPrint().encodedJson().execute(); + + assertEquals("http://example.com/fhir/Location/123?_pretty=true", capt.getAllValues().get(1).getURI().toString()); + assertEquals("DELETE", capt.getValue().getMethod()); + assertEquals(null, outcome); + + } + + @Test + public void testHistory() 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; + + response = client + .history() + .onServer() + .andReturnBundle(Bundle.class) + .execute(); + assertEquals("http://example.com/fhir/_history", capt.getAllValues().get(idx).getURI().toString()); + assertEquals(1, response.getEntry().size()); + idx++; + + response = client + .history() + .onType(Patient.class) + .andReturnBundle(Bundle.class) + .execute(); + assertEquals("http://example.com/fhir/Patient/_history", capt.getAllValues().get(idx).getURI().toString()); + assertEquals(1, response.getEntry().size()); + idx++; + + response = client + .history() + .onInstance(new IdType("Patient", "123")) + .andReturnBundle(Bundle.class) + .execute(); + assertEquals("http://example.com/fhir/Patient/123/_history", capt.getAllValues().get(idx).getURI().toString()); + assertEquals(1, response.getEntry().size()); + 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 { + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); + 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(getPatientFeedWithOneResult()), Charset.forName("UTF-8"))); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); + + client.search().forResource("Patient").where(Patient.NAME.isMissing(true)).returnBundle(Bundle.class).execute(); + assertEquals("http://example.com/fhir/Patient?name%3Amissing=true", capt.getValue().getRequestLine().getUri()); + + client.search().forResource("Patient").where(Patient.NAME.isMissing(false)).returnBundle(Bundle.class).execute(); + assertEquals("http://example.com/fhir/Patient?name%3Amissing=false", capt.getValue().getRequestLine().getUri()); + } + + @Test + public void testRead() throws Exception { + + String msg = getResourceResult(); + + 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"))); + Header[] headers = new Header[] { + new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 GMT"), + new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333"), + }; + when(myHttpResponse.getAllHeaders()).thenReturn(headers); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Patient response = client + .read() + .resource(Patient.class) + .withId(new IdType("Patient/1234")) + .execute(); + + assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); + + assertEquals("http://foo.com/Patient/123/_history/2333", response.getIdElement().getValue()); + + InstantType lm = response.getMeta().getLastUpdatedElement(); + lm.setTimeZoneZulu(true); + assertEquals("1995-11-15T04:58:08.000Z", lm.getValueAsString()); + + } + + @Test + public void testReadFluent() throws Exception { + + String msg = getResourceResult(); + + 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"))); + Header[] headers = new Header[] { + new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 GMT"), + new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333"), + }; + when(myHttpResponse.getAllHeaders()).thenReturn(headers); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + int count = 0; + + Patient response = client.read().resource(Patient.class).withId(new IdType("Patient/1234")).execute(); + assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); + assertEquals("http://example.com/fhir/Patient/1234", capt.getAllValues().get(count++).getURI().toString()); + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + response = (Patient) client.read().resource("Patient").withId("1234").execute(); + assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); + assertEquals("http://example.com/fhir/Patient/1234", capt.getAllValues().get(count++).getURI().toString()); + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + response = (Patient) client.read().resource("Patient").withId(567L).execute(); + assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); + assertEquals("http://example.com/fhir/Patient/567", capt.getAllValues().get(count++).getURI().toString()); + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + response = client.read().resource(Patient.class).withIdAndVersion("1234", "22").execute(); + assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); + assertEquals("http://example.com/fhir/Patient/1234/_history/22", capt.getAllValues().get(count++).getURI().toString()); + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + response = client.read().resource(Patient.class).withUrl("http://foo/Patient/22").execute(); + assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); + assertEquals("http://foo/Patient/22", capt.getAllValues().get(count++).getURI().toString()); + + } + + @Test + public void testReadWithAbsoluteUrl() throws Exception { + + String msg = getResourceResult(); + + 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"))); + Header[] headers = new Header[] { new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 GMT"), + new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333"), + }; + when(myHttpResponse.getAllHeaders()).thenReturn(headers); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Patient response = client + .read() + .resource(Patient.class) + .withUrl(new IdType("http://somebase.com/path/to/base/Patient/1234")) + .execute(); + assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); + assertEquals("http://somebase.com/path/to/base/Patient/1234", capt.getAllValues().get(0).getURI().toString()); + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + response = client + .read() + .resource(Patient.class) + .withUrl(new IdType("http://somebase.com/path/to/base/Patient/1234/_history/222")) + .execute(); + assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); + assertEquals("http://somebase.com/path/to/base/Patient/1234/_history/222", capt.getAllValues().get(1).getURI().toString()); + + } + + @SuppressWarnings("unused") + @Test + public void testSearchAllResources() throws Exception { + + 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client.search() + .forAllResources() + .where(Patient.NAME.matches().value("james")) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/?name=james", capt.getValue().getURI().toString()); + + } + + @SuppressWarnings("unused") + @Test + public void testSearchAutomaticallyUsesPost() throws Exception { + + 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + String longValue = StringUtils.leftPad("", 20000, 'B'); + + Bundle response = client.search() + .forResource("Patient") + .where(Patient.NAME.matches().value(longValue)) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient/_search", capt.getValue().getURI().toString()); + + HttpEntityEnclosingRequestBase enc = (HttpEntityEnclosingRequestBase) capt.getValue(); + UrlEncodedFormEntity ent = (UrlEncodedFormEntity) enc.getEntity(); + String string = IOUtils.toString(ent.getContent()); + ourLog.info(string); + assertEquals("name=" + longValue, string); + } + + @Test + public void testSearchByCompartment() 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"); + Bundle response = client + .search() + .forResource(Patient.class) + .withIdAndCompartment("123", "fooCompartment") + .where(Patient.BIRTHDATE.afterOrEquals().day("2011-01-02")) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://foo/Patient/123/fooCompartment?birthdate=ge2011-01-02", capt.getValue().getURI().toString()); + + ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(response)); + + assertEquals("PRP1660", BundleUtil.toListOfResourcesOfType(ourCtx, response, Patient.class).get(0).getIdentifier().get(0).getValue()); + + try { + client + .search() + .forResource(Patient.class) + .withIdAndCompartment("", "fooCompartment") + .where(Patient.BIRTHDATE.afterOrEquals().day("2011-01-02")) + .returnBundle(Bundle.class) + .execute(); + fail(); + } catch (InvalidRequestException e) { + assertThat(e.toString(), containsString("null or empty for compartment")); + } + + } + + @SuppressWarnings("unused") + @Test + public void testSearchByComposite() throws Exception { + + 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://foo"); + + Bundle response = client.search() + .forResource("Observation") + .where(Observation.CODE_VALUE_DATE + .withLeft(Observation.CODE.exactly().code("FOO$BAR")) + .withRight(Observation.VALUE_DATE.exactly().day("2001-01-01"))) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://foo/Observation?" + Observation.SP_CODE_VALUE_DATE + "=" + URLEncoder.encode("FOO\\$BAR$2001-01-01", "UTF-8"), capt.getValue().getURI().toString()); + + } + + @SuppressWarnings("unused") + @Test + public void testSearchByDate() 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; + + @SuppressWarnings("deprecation") + Bundle response = client.search() + .forResource(Patient.class) + .encodedJson() + .where(Patient.BIRTHDATE.beforeOrEquals().day("2012-01-22")) + .and(Patient.BIRTHDATE.after().day("2011-01-01")) + .include(Patient.INCLUDE_ORGANIZATION) + .sort().ascending(Patient.BIRTHDATE) + .sort().descending(Patient.NAME) + .sort().defaultOrder(Patient.ADDRESS) + .count(123) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient?birthdate=le2012-01-22&birthdate=gt2011-01-01&_include=Patient%3Aorganization&_sort=birthdate%2C-name%2Caddress&_count=123&_format=json", + capt.getAllValues().get(idx++).getURI().toString()); + + response = client.search() + .forResource(Patient.class) + .encodedJson() + .where(Patient.BIRTHDATE.beforeOrEquals().day("2012-01-22")) + .and(Patient.BIRTHDATE.after().day("2011-01-01")) + .include(Patient.INCLUDE_ORGANIZATION) + .sort().ascending(Patient.BIRTHDATE) + .sort().descending(Patient.NAME) + .sort().defaultOrder(Patient.ADDRESS) + .count(123) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient?birthdate=le2012-01-22&birthdate=gt2011-01-01&_include=Patient%3Aorganization&_sort=birthdate%2C-name%2Caddress&_count=123&_format=json", + capt.getAllValues().get(idx++).getURI().toString()); + + response = client.search() + .forResource(Patient.class) + .encodedJson() + .where(Patient.BIRTHDATE.beforeOrEquals().day("2012-01-22").orAfter().day("2020-01-01")) + .and(Patient.BIRTHDATE.after().day("2011-01-01")) + .returnBundle(Bundle.class) + .execute(); + + String comma = "%2C"; + assertEquals("http://example.com/fhir/Patient?birthdate=le2012-01-22" + comma + "gt2020-01-01&birthdate=gt2011-01-01&_format=json", capt.getAllValues().get(idx++).getURI().toString()); + } + + @SuppressWarnings("unused") + @Test + public void testSearchByNumberExact() 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) + .where(Observation.VALUE_QUANTITY.greaterThan().number(123).andUnits("foo", "bar")) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Observation?value-quantity=gt123%7Cfoo%7Cbar", capt.getValue().getURI().toString()); + } + + @SuppressWarnings("unused") + @Test + public void testSearchByProfile() throws Exception { + + 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client.search() + .forResource(Patient.class) + .withProfile("http://1") + .withProfile("http://2") + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient?_profile=http%3A%2F%2F1&_profile=http%3A%2F%2F2", capt.getValue().getURI().toString()); + + } + + @SuppressWarnings("unused") + @Test + public void testSearchByQuantity() throws Exception { + + 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client.search() + .forResource(Patient.class) + .where(Encounter.LENGTH.exactly().number(123)) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient?length=123", capt.getValue().getURI().toString()); + + } + + @SuppressWarnings("unused") + @Test + public void testSearchByReferenceProperty() throws Exception { + + 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client.search() + .forResource(Patient.class) + .where(Patient.GENERAL_PRACTITIONER.hasChainedProperty(Organization.NAME.matches().value("ORG0"))) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient?general-practitioner.name=ORG0", capt.getValue().getURI().toString()); + + } + + @SuppressWarnings("unused") + @Test + public void testSearchByReferenceSimple() throws Exception { + + 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client.search() + .forResource("Patient") + .where(Patient.GENERAL_PRACTITIONER.hasId("123")) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient?general-practitioner=123", capt.getValue().getURI().toString()); + + } + + @SuppressWarnings("unused") + @Test + public void testSearchBySecurity() throws Exception { + + 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client.search() + .forResource(Patient.class) + .withSecurity("urn:foo", "123") + .withSecurity("urn:bar", "456") + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient?_security=urn%3Afoo%7C123&_security=urn%3Abar%7C456", capt.getValue().getURI().toString()); + + } + + @SuppressWarnings("unused") + @Test + public void testSearchByString() throws Exception { + + 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client.search() + .forResource("Patient") + .where(Patient.NAME.matches().value("james")) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient?name=james", capt.getValue().getURI().toString()); + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + response = client.search() + .forResource("Patient") + .where(Patient.NAME.matches().values("AAA", "BBB", "C,C")) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient?name=" + URLEncoder.encode("AAA,BBB,C\\,C", "UTF-8"), capt.getAllValues().get(1).getURI().toString()); + + } + + @SuppressWarnings("unused") + @Test + public void testSearchByStringExact() throws Exception { + + 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client.search() + .forResource("Patient") + .where(Patient.NAME.matchesExactly().value("james")) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient?name%3Aexact=james", capt.getValue().getURI().toString()); + + } + + @SuppressWarnings("unused") + @Test + public void testSearchByTag() throws Exception { + + 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client.search() + .forResource(Patient.class) + .withTag("urn:foo", "123") + .withTag("urn:bar", "456") + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient?_tag=urn%3Afoo%7C123&_tag=urn%3Abar%7C456", capt.getValue().getURI().toString()); + + } + + @SuppressWarnings("unused") + @Test + public void testSearchByToken() throws Exception { + + 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client.search() + .forResource("Patient") + .where(Patient.IDENTIFIER.exactly().systemAndCode("http://example.com/fhir", "ZZZ")) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient?identifier=http%3A%2F%2Fexample.com%2Ffhir%7CZZZ", capt.getValue().getURI().toString()); + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + response = client.search() + .forResource("Patient") + .where(Patient.IDENTIFIER.exactly().code("ZZZ")) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient?identifier=ZZZ", capt.getAllValues().get(1).getURI().toString()); + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + response = client.search() + .forResource("Patient") + .where(Patient.IDENTIFIER.exactly().codings(new Coding("A", "B", "ZZZ"), new Coding("C", "D", "ZZZ"))) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient?identifier=" + URLEncoder.encode("A|B,C|D", "UTF-8"), capt.getAllValues().get(2).getURI().toString()); + + } + + /** + * Test for #192 + */ + @SuppressWarnings("unused") + @Test + public void testSearchByTokenWithEscaping() 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://foo"); + int index = 0; + String wantPrefix = "http://foo/Patient?identifier="; + + Bundle response = client.search() + .forResource("Patient") + .where(Patient.IDENTIFIER.exactly().systemAndCode("1", "2")) + .returnBundle(Bundle.class) + .execute(); + String wantValue = "1|2"; + String url = capt.getAllValues().get(index).getURI().toString(); + assertThat(url, Matchers.startsWith(wantPrefix)); + assertEquals(wantValue, UrlUtil.unescape(url.substring(wantPrefix.length()))); + assertEquals(UrlUtil.escape(wantValue), url.substring(wantPrefix.length())); + index++; + + response = client.search() + .forResource("Patient") + .where(Patient.IDENTIFIER.exactly().systemAndCode("1,2", "3,4")) + .returnBundle(Bundle.class) + .execute(); + wantValue = "1\\,2|3\\,4"; + url = capt.getAllValues().get(index).getURI().toString(); + assertThat(url, Matchers.startsWith(wantPrefix)); + assertEquals(wantValue, UrlUtil.unescape(url.substring(wantPrefix.length()))); + assertEquals(UrlUtil.escape(wantValue), url.substring(wantPrefix.length())); + 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 { + + 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client.search() + .forResource(Patient.class) + .include(Patient.INCLUDE_ORGANIZATION) + .include(Patient.INCLUDE_LINK.asRecursive()) + .include(Patient.INCLUDE_ALL.asNonRecursive()) + .returnBundle(Bundle.class) + .execute(); + + assertThat(capt.getValue().getURI().toString(), containsString("http://example.com/fhir/Patient?")); + assertThat(capt.getValue().getURI().toString(), containsString("_include=" + UrlUtil.escape(Patient.INCLUDE_ORGANIZATION.getValue()))); + assertThat(capt.getValue().getURI().toString(), containsString("_include%3Arecurse=" + UrlUtil.escape(Patient.INCLUDE_LINK.getValue()))); + assertThat(capt.getValue().getURI().toString(), containsString("_include=*")); + + } + + @SuppressWarnings("unused") + @Test + public void testSearchUsingGetSearch() throws Exception { + + 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client.search() + .forResource("Patient") + .where(Patient.NAME.matches().value("james")) + .usingStyle(SearchStyleEnum.GET_WITH_SEARCH) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient/_search?name=james", capt.getValue().getURI().toString()); + } + + @SuppressWarnings("unused") + @Test + public void testSearchUsingPost() throws Exception { + + 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client.search() + .forResource("Patient") + .where(Patient.NAME.matches().value("james")) + .usingStyle(SearchStyleEnum.POST) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient/_search", capt.getValue().getURI().toString()); + + HttpEntityEnclosingRequestBase enc = (HttpEntityEnclosingRequestBase) capt.getValue(); + UrlEncodedFormEntity ent = (UrlEncodedFormEntity) enc.getEntity(); + String string = IOUtils.toString(ent.getContent()); + ourLog.info(string); + assertEquals("name=james", string); + } + + @Test + public void testSearchWithAbsoluteUrl() throws Exception { + + 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client + .search() + .byUrl("http://example.com/fhir/Patient?birthdate=le2012-01-22&birthdate=gt2011-01-01&_include=Patient%3Aorganization&_sort%3Aasc=birthdate&_sort%3Adesc=name&_count=123&_format=json") + .returnBundle(Bundle.class) + .execute(); + + assertEquals(1, response.getEntry().size()); + } + + @SuppressWarnings("unused") + @Test + public void testSearchWithClientEncodingAndPrettyPrintConfig() throws Exception { + + 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + client.setPrettyPrint(true); + client.setEncoding(EncodingEnum.JSON); + + Bundle response = client.search() + .forResource(Patient.class) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient?_format=json&_pretty=true", capt.getValue().getURI().toString()); + + } + + @SuppressWarnings("unused") + @Test + public void testSearchWithEscapedParameters() throws Exception { + + 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client.search() + .forResource("Patient") + .where(Patient.NAME.matches().values("NE,NE", "NE,NE")) + .where(Patient.NAME.matchesExactly().values("E$E")) + .where(Patient.NAME.matches().values("NE\\NE")) + .where(Patient.NAME.matchesExactly().values("E|E")) + .returnBundle(Bundle.class) + .execute(); + + assertThat(capt.getValue().getURI().toString(), containsString("%3A")); + assertEquals("http://example.com/fhir/Patient?name=NE\\,NE,NE\\,NE&name=NE\\\\NE&name:exact=E\\$E&name:exact=E\\|E", UrlUtil.unescape(capt.getValue().getURI().toString())); + } + + @SuppressWarnings("unused") + @Test + public void testSearchWithInternalServerError() throws Exception { + + 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), 500, "INTERNAL ERRORS")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader("Server Issues!"), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + try { + client + .search() + .forResource(Patient.class) + .returnBundle(Bundle.class) + .execute(); + fail(); + } catch (InternalErrorException e) { + assertEquals(e.getMessage(), "HTTP 500 INTERNAL ERRORS: Server Issues!"); + assertEquals(e.getResponseBody(), "Server Issues!"); + } + + } + + @SuppressWarnings("unused") + @Test + public void testSearchWithNonFhirResponse() throws Exception { + + 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_TEXT + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader("Server Issues!"), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + try { + client.search().forResource(Patient.class).returnBundle(Bundle.class).execute(); + fail(); + } catch (NonFhirResponseException e) { + assertThat(e.getMessage(), StringContains.containsString("Server Issues!")); + } + + } + + @SuppressWarnings("unused") + @Test + public void testSearchWithReverseInclude() throws Exception { + + 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client.search() + .forResource(Patient.class) + .encodedJson() + .revInclude(Provenance.INCLUDE_TARGET) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient?_revinclude=Provenance%3Atarget&_format=json", capt.getValue().getURI().toString()); + + } + + @Test + public void testSetDefaultEncoding() throws Exception { + + String msg = ourCtx.newJsonParser().encodeResourceToString(new Patient()); + + 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_JSON + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + // Header[] headers = new Header[] { new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 + // GMT"), + // new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333"), + // new BasicHeader(Constants.HEADER_CATEGORY, "http://foo/tagdefinition.html; scheme=\"http://hl7.org/fhir/tag\"; + // label=\"Some tag\"") }; + // when(myHttpResponse.getAllHeaders()).thenReturn(headers); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + (client).setEncoding(EncodingEnum.JSON); + int count = 0; + + client + .read() + .resource(Patient.class) + .withId(new IdType("Patient/1234")) + .execute(); + assertEquals("http://example.com/fhir/Patient/1234?_format=json", capt.getAllValues().get(count).getURI().toString()); + count++; + + } + + @Test + public void testTransaction() throws Exception { + Bundle input = createTransactionBundleInput(); + Bundle output = createTransactionBundleOutput(); + + String msg = ourCtx.newJsonParser().encodeResourceToString(output); + + 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_JSON + "; 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.transaction() + .withBundle(input) + .execute(); + + assertEquals("http://example.com/fhir", capt.getValue().getURI().toString()); + assertEquals(input.getEntry().get(0).getResource().getId(), response.getEntry().get(0).getResource().getId()); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(0).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + + } + + @Test + public void testTransactionXml() throws Exception { + Bundle input = createTransactionBundleInput(); + Bundle output = createTransactionBundleOutput(); + + String msg = ourCtx.newXmlParser().encodeResourceToString(output); + + 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.transaction() + .withBundle(input) + .execute(); + + assertEquals("http://example.com/fhir", capt.getValue().getURI().toString()); + assertEquals(input.getEntry().get(0).getResource().getId(), response.getEntry().get(0).getResource().getId()); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(0).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + + } + + @Test + public void testUpdate() throws Exception { + + Patient p1 = new Patient(); + p1.addIdentifier().setSystem("foo:bar").setValue("12345"); + p1.addName().setFamily("Smith").addGiven("John"); + + 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), 201, "OK")); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); + 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(""), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + try { + client.update().resource(p1).execute(); + fail(); + } catch (InvalidRequestException e) { + // should happen because no ID set + } + + assertEquals(0, capt.getAllValues().size()); + + p1.setId("44"); + client.update().resource(p1).execute(); + + int count = 0; + + assertEquals(1, capt.getAllValues().size()); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + count++; + + MethodOutcome outcome = client.update().resource(p1).execute(); + assertEquals("44", outcome.getId().getIdPart()); + assertEquals("22", outcome.getId().getVersionIdPart()); + + assertEquals(2, capt.getAllValues().size()); + + assertEquals("http://example.com/fhir/Patient/44", capt.getValue().getURI().toString()); + assertEquals("PUT", capt.getValue().getMethod()); + + /* + * Try fluent options + */ + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + client.update().resource(p1).withId("123").execute(); + assertEquals(3, capt.getAllValues().size()); + assertEquals("http://example.com/fhir/Patient/123", capt.getAllValues().get(2).getURI().toString()); + + String resourceText = "<Patient xmlns=\"http://hl7.org/fhir\"> </Patient>"; + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + client.update().resource(resourceText).withId("123").execute(); + assertEquals("http://example.com/fhir/Patient/123", capt.getAllValues().get(3).getURI().toString()); + assertEquals(resourceText, IOUtils.toString(((HttpPut) capt.getAllValues().get(3)).getEntity().getContent())); + assertEquals(4, capt.getAllValues().size()); + + } + + @Test + public void testUpdateWithStringAutoDetectsEncoding() throws Exception { + + Patient p1 = new Patient(); + p1.addIdentifier().setSystem("foo:bar").setValue("12345"); + p1.addName().setFamily("Smith").addGiven("John"); + + 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), 201, "OK")); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); + 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(""), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + int count = 0; + client.update().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).withId("1").execute(); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + assertThat(extractBody(capt, count), containsString("value=\"John\"")); + count++; + + client.update().resource(ourCtx.newJsonParser().encodeResourceToString(p1)).withId("1").execute(); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.JSON.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + assertThat(extractBody(capt, count), containsString("[\"John\"]")); + count++; + + /* + * e.g. Now try with reversed encoding (provide a string that's in JSON and ask the client to use XML) + */ + + client.update().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).withId("1").encodedJson().execute(); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.JSON.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + assertThat(extractBody(capt, count), containsString("[\"John\"]")); + count++; + + client.update().resource(ourCtx.newJsonParser().encodeResourceToString(p1)).withId("1").encodedXml().execute(); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + assertThat(extractBody(capt, count), containsString("value=\"John\"")); + count++; + } + + @Test + public void testVReadWithAbsoluteUrl() throws Exception { + + String msg = getResourceResult(); + + 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"))); + Header[] headers = new Header[] { + new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 GMT"), + new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333"), + }; + when(myHttpResponse.getAllHeaders()).thenReturn(headers); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Patient response = client + .read() + .resource(Patient.class) + .withUrl("http://somebase.com/path/to/base/Patient/1234/_history/2222") + .execute(); + + assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); + assertEquals("http://somebase.com/path/to/base/Patient/1234/_history/2222", capt.getAllValues().get(0).getURI().toString()); + + } + + @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 + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + @BeforeClass + public static void beforeClass() { + ourCtx = FhirContext.forR4(); + } + +} diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 4cca7e235fc..3e9ed722ff6 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -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"> diff --git a/src/site/xdoc/doc_jpa.xml b/src/site/xdoc/doc_jpa.xml index ae2a9b8db84..d1eb338dfa9 100644 --- a/src/site/xdoc/doc_jpa.xml +++ b/src/site/xdoc/doc_jpa.xml @@ -1,315 +1,372 @@ -<?xml version="1.0" encoding="UTF-8"?> -<document xmlns="http://maven.apache.org/XDOC/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd"> - - <properties> - <title>JPA Server - James Agnew - - - - -
- -

- 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:
    - $ mvn jetty:run - 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-*"); - -
- -
- -
- - Architecture - -

- 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. -
  • -
- -
- - - - - - + + + + + JPA Server + James Agnew + + + + +
+ +

+ 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:
    + $ mvn jetty:run + 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: +

+
+
+					myDaoConfig.setReuseCachedSearchResultsForMillis(null);
+				
+
+

Disable Cache at the Request Level

+

+ 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
+					
+
+ +
+ +
+ +
+ + Architecture + +

+ 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. +
  • +
+ +
+ + + + + +
diff --git a/src/site/xdoc/doc_rest_client.xml b/src/site/xdoc/doc_rest_client.xml index 28dc0366781..7a350d2a808 100644 --- a/src/site/xdoc/doc_rest_client.xml +++ b/src/site/xdoc/doc_rest_client.xml @@ -1,539 +1,567 @@ - - - - - RESTful Client - James Agnew - - - - - -
- - - -

- 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. -

- - - - - - -
- -
- - - -
+ + + + + RESTful Client + James Agnew + + + + + +
+ + + +

+ 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: +

+ + + + + + +
+ +
+ + + +