Ongoing work on interceptors

This commit is contained in:
James Agnew 2018-07-24 17:22:21 +07:00
parent 23083a9283
commit 3c80238f0e
13 changed files with 584 additions and 124 deletions

View File

@ -189,9 +189,11 @@ public class Constants {
public static final String HEADER_X_CACHE = "X-Cache";
public static final String HEADER_X_SECURITY_CONTEXT = "X-Security-Context";
public static final String POWERED_BY_HEADER = "X-Powered-By";
public static final Charset CHARSET_US_ASCII;
static {
CHARSET_UTF8 = Charset.forName(CHARSET_NAME_UTF8);
CHARSET_US_ASCII = Charset.forName("ISO-8859-1");
HashMap<Integer, String> statusNames = new HashMap<>();
statusNames.put(200, "OK");

View File

@ -1,9 +1,10 @@
package ca.uhn.fhir.rest.gclient;
import java.util.*;
import ca.uhn.fhir.model.api.IQueryParameterType;
import java.util.List;
import java.util.Map;
/*
* #%L
* HAPI FHIR - Core Library
@ -13,9 +14,9 @@ import ca.uhn.fhir.model.api.IQueryParameterType;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -26,10 +27,32 @@ import ca.uhn.fhir.model.api.IQueryParameterType;
public interface IBaseQuery<T extends IBaseQuery<?>> {
T where(ICriterion<?> theCriterion);
/**
* Add a search parameter to the query.
* <p>
* Note that this method is a synonym for {@link #where(ICriterion)}, and is only
* here to make fluent queries read more naturally.
* </p>
*/
T and(ICriterion<?> theCriterion);
T where(Map<String, List<IQueryParameterType>> theCriterion);
/**
* Add a set of search parameters to the query.
*/
T where(Map<String, List<IQueryParameterType>> theCriterion);
T and(ICriterion<?> theCriterion);
/**
* Add a search parameter to the query.
*/
T where(ICriterion<?> theCriterion);
/**
* Add a set of search parameters to the query.
* <p>
* Values will be treated semi-literally. No FHIR escaping will be performed
* on the values, but regular URL escaping will be.
* </p>
*/
T whereMap(Map<String, List<String>> theRawMap);
}

View File

@ -1,6 +1,14 @@
package ca.uhn.fhir.rest.gclient;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.api.SearchStyleEnum;
import ca.uhn.fhir.rest.api.SortSpec;
import ca.uhn.fhir.rest.param.DateRangeParam;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/*
* #%L
@ -11,9 +19,9 @@ import java.util.Collection;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -22,14 +30,23 @@ import java.util.Collection;
* #L%
*/
import org.hl7.fhir.instance.model.api.IBaseBundle;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.api.SearchStyleEnum;
import ca.uhn.fhir.rest.param.DateRangeParam;
public interface IQuery<Y> extends IBaseQuery<IQuery<Y>>, IClientExecutable<IQuery<Y>, Y> {
/**
* {@inheritDoc}
*/
// This is here as an overridden method to allow mocking clients with Mockito to work
@Override
IQuery<Y> and(ICriterion<?> theCriterion);
/**
* Specifies the <code>_count</code> parameter, which indicates to the server how many resources should be returned
* on a single page.
*
* @since 1.4
*/
IQuery<Y> count(int theCount);
/**
* Add an "_include" specification or an "_include:recurse" specification. If you are using
* a constant from one of the built-in structures you can select whether you want recursive
@ -41,88 +58,60 @@ public interface IQuery<Y> extends IBaseQuery<IQuery<Y>>, IClientExecutable<IQue
*/
IQuery<Y> include(Include theInclude);
ISort<Y> sort();
/**
* Add a "_lastUpdated" specification
*
* @since HAPI FHIR 1.1 - Note that option was added to FHIR itself in DSTU2
*/
IQuery<Y> lastUpdated(DateRangeParam theLastUpdated);
/**
* Specifies the <code>_count</code> parameter, which indicates to the server how many resources should be returned
* on a single page.
*
*
* @deprecated This parameter is badly named, since FHIR calls this parameter "_count" and not "_limit". Use {@link #count(int)} instead (it also sets the _count parameter)
*/
@Deprecated
IQuery<Y> limitTo(int theLimitTo);
/**
* Specifies the <code>_count</code> parameter, which indicates to the server how many resources should be returned
* on a single page.
*
* @since 1.4
* Request that the client return the specified bundle type, e.g. <code>org.hl7.fhir.instance.model.Bundle.class</code>
* or <code>ca.uhn.fhir.model.dstu2.resource.Bundle.class</code>
*/
IQuery<Y> count(int theCount);
/**
* Match only resources where the resource has the given tag. This parameter corresponds to
* the <code>_tag</code> URL parameter.
* @param theSystem The tag code system, or <code>null</code> to match any code system (this may not be supported on all servers)
* @param theCode The tag code. Must not be <code>null</code> or empty.
*/
IQuery<Y> withTag(String theSystem, String theCode);
/**
* Match only resources where the resource has the given security tag. This parameter corresponds to
* the <code>_security</code> URL parameter.
* @param theSystem The tag code system, or <code>null</code> to match any code system (this may not be supported on all servers)
* @param theCode The tag code. Must not be <code>null</code> or empty.
*/
IQuery<Y> withSecurity(String theSystem, String theCode);
/**
* Match only resources where the resource has the given profile declaration. This parameter corresponds to
* the <code>_profile</code> URL parameter.
* @param theProfileUri The URI of a given profile to search for resources which match
*/
IQuery<Y> withProfile(String theProfileUri);
/**
* Matches any of the profiles given as argument. This would result in an OR search for resources matching one or more profiles.
* To do an AND search, make multiple calls to {@link #withProfile(String)}.
* @param theProfileUris The URIs of a given profile to search for resources which match.
*/
IQuery<Y> withAnyProfile(Collection<String> theProfileUris);
/**
* Forces the query to perform the search using the given method (allowable methods are described in the
* <a href="http://www.hl7.org/fhir/search.html">FHIR Search Specification</a>)
* <p>
* This can be used to force the use of an HTTP POST instead of an HTTP GET
* </p>
*
* @see SearchStyleEnum
* @since 0.6
*/
IQuery<Y> usingStyle(SearchStyleEnum theStyle);
IQuery<Y> withIdAndCompartment(String theResourceId, String theCompartmentName);
<B extends IBaseBundle> IQuery<B> returnBundle(Class<B> theClass);
/**
* Add a "_revinclude" specification
*
*
* @since HAPI FHIR 1.0 - Note that option was added to FHIR itself in DSTU2
*/
IQuery<Y> revInclude(Include theIncludeTarget);
/**
* Add a "_lastUpdated" specification
*
* @since HAPI FHIR 1.1 - Note that option was added to FHIR itself in DSTU2
* Adds a sort criteria
*
* @see #sort(SortSpec) for an alternate way of speciyfing sorts
*/
IQuery<Y> lastUpdated(DateRangeParam theLastUpdated);
ISort<Y> sort();
/**
* Request that the client return the specified bundle type, e.g. <code>org.hl7.fhir.instance.model.Bundle.class</code>
* or <code>ca.uhn.fhir.model.dstu2.resource.Bundle.class</code>
* Adds a sort using a {@link SortSpec} object
*
* @see #sort() for an alternate way of speciyfing sorts
*/
<B extends IBaseBundle> IQuery<B> returnBundle(Class<B> theClass);
IQuery<Y> sort(SortSpec theSortSpec);
/**
* Forces the query to perform the search using the given method (allowable methods are described in the
* <a href="http://www.hl7.org/fhir/search.html">FHIR Search Specification</a>)
* <p>
* This can be used to force the use of an HTTP POST instead of an HTTP GET
* </p>
*
* @see SearchStyleEnum
* @since 0.6
*/
IQuery<Y> usingStyle(SearchStyleEnum theStyle);
/**
* {@inheritDoc}
@ -132,11 +121,40 @@ public interface IQuery<Y> extends IBaseQuery<IQuery<Y>>, IClientExecutable<IQue
IQuery<Y> where(ICriterion<?> theCriterion);
/**
* {@inheritDoc}
* Matches any of the profiles given as argument. This would result in an OR search for resources matching one or more profiles.
* To do an AND search, make multiple calls to {@link #withProfile(String)}.
*
* @param theProfileUris The URIs of a given profile to search for resources which match.
*/
// This is here as an overridden method to allow mocking clients with Mockito to work
@Override
IQuery<Y> and(ICriterion<?> theCriterion);
IQuery<Y> withAnyProfile(Collection<String> theProfileUris);
IQuery<Y> withIdAndCompartment(String theResourceId, String theCompartmentName);
/**
* Match only resources where the resource has the given profile declaration. This parameter corresponds to
* the <code>_profile</code> URL parameter.
*
* @param theProfileUri The URI of a given profile to search for resources which match
*/
IQuery<Y> withProfile(String theProfileUri);
/**
* Match only resources where the resource has the given security tag. This parameter corresponds to
* the <code>_security</code> URL parameter.
*
* @param theSystem The tag code system, or <code>null</code> to match any code system (this may not be supported on all servers)
* @param theCode The tag code. Must not be <code>null</code> or empty.
*/
IQuery<Y> withSecurity(String theSystem, String theCode);
/**
* Match only resources where the resource has the given tag. This parameter corresponds to
* the <code>_tag</code> URL parameter.
*
* @param theSystem The tag code system, or <code>null</code> to match any code system (this may not be supported on all servers)
* @param theCode The tag code. Must not be <code>null</code> or empty.
*/
IQuery<Y> withTag(String theSystem, String theCode);
// Y execute();

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.gclient;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -23,13 +23,13 @@ package ca.uhn.fhir.rest.gclient;
public interface ISort<T> {
/**
* Sort ascending
* Sort ascending
*/
IQuery<T> ascending(IParam theParam);
/**
* Sort ascending
*
* Sort ascending
*
* @param theParam The param name, e.g. "address"
*/
IQuery<T> ascending(String theParam);
@ -37,22 +37,30 @@ public interface ISort<T> {
/**
* Sort by the default order. Note that as of STU3, there is no longer
* a concept of default order, only ascending and descending. This method
* technically implies "ascending" but it makes more sense to use
* technically implies "ascending" but it makes more sense to use
* {@link #ascending(IParam)}
*/
IQuery<T> defaultOrder(IParam theParam);
/**
* Sort by the default order. Note that as of STU3, there is no longer
* a concept of default order, only ascending and descending. This method
* technically implies "ascending" but it makes more sense to use
* {@link #ascending(IParam)}
*/
IQuery<T> defaultOrder(String theParam);
/**
* Sort descending
*
* @param theParam A query param - Could be a constant such as <code>Patient.ADDRESS</code> or a custom
* param such as <code>new StringClientParam("foo")</code>
*
* @param theParam A query param - Could be a constant such as <code>Patient.ADDRESS</code> or a custom
* param such as <code>new StringClientParam("foo")</code>
*/
IQuery<T> descending(IParam theParam);
/**
* Sort ascending
*
* Sort ascending
*
* @param theParam The param name, e.g. "address"
*/
IQuery<T> descending(String theParam);

View File

@ -316,7 +316,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
private static void addParam(Map<String, List<String>> params, String parameterName, String parameterValue) {
if (!params.containsKey(parameterName)) {
params.put(parameterName, new ArrayList<String>());
params.put(parameterName, new ArrayList<>());
}
params.get(parameterName).add(parameterValue);
}
@ -516,6 +516,19 @@ public class GenericClient extends BaseClient implements IGenericClient {
return (QUERY) this;
}
@Override
public QUERY whereMap(Map<String, List<String>> theRawMap) {
if (theRawMap != null) {
for (String nextKey : theRawMap.keySet()) {
for (String nextValue : theRawMap.get(nextKey)) {
addParam(myParams, nextKey, nextValue);
}
}
}
return (QUERY) this;
}
@SuppressWarnings("unchecked")
@Override
public QUERY where(Map<String, List<IQueryParameterType>> theCriterion) {
@ -1854,6 +1867,16 @@ public class GenericClient extends BaseClient implements IGenericClient {
return retVal;
}
@Override
public IQuery sort(SortSpec theSortSpec) {
SortSpec sortSpec = theSortSpec;
while (sortSpec != null) {
mySort.add(new SortInternal(sortSpec));
sortSpec = sortSpec.getChain();
}
return this;
}
@Override
public IQuery usingStyle(SearchStyleEnum theStyle) {
mySearchStyle = theStyle;
@ -2140,6 +2163,18 @@ public class GenericClient extends BaseClient implements IGenericClient {
myFor = theFor;
}
public SortInternal(SortSpec theSortSpec) {
if (theSortSpec.getOrder() == null) {
myParamName = Constants.PARAM_SORT;
} else if (theSortSpec.getOrder() == SortOrderEnum.ASC) {
myParamName = Constants.PARAM_SORT_ASC;
} else if (theSortSpec.getOrder() == SortOrderEnum.DESC) {
myParamName = Constants.PARAM_SORT_DESC;
}
myDirection = theSortSpec.getOrder();
myParamValue = theSortSpec.getParamName();
}
@Override
public IQuery ascending(IParam theParam) {
myParamName = Constants.PARAM_SORT_ASC;
@ -2164,6 +2199,14 @@ public class GenericClient extends BaseClient implements IGenericClient {
return myFor;
}
@Override
public IQuery defaultOrder(String theParam) {
myParamName = Constants.PARAM_SORT;
myDirection = null;
myParamValue = theParam;
return myFor;
}
@Override
public IQuery descending(IParam theParam) {
myParamName = Constants.PARAM_SORT_DESC;

View File

@ -29,6 +29,7 @@ import org.apache.commons.lang3.StringUtils;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.client.api.*;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.apache.commons.lang3.Validate;
/**
* HTTP interceptor to be used for adding HTTP basic auth username/password tokens
@ -42,23 +43,29 @@ public class BasicAuthInterceptor implements IClientInterceptor {
private String myUsername;
private String myPassword;
private String myHeaderValue;
public BasicAuthInterceptor(String theUsername, String thePassword) {
super();
myUsername = theUsername;
myPassword = thePassword;
/**
* @param theUsername The username
* @param thePassword The password
*/
public BasicAuthInterceptor(String theUsername, String thePassword) {
this(StringUtils.defaultString(theUsername) + ":" + StringUtils.defaultString(thePassword));
}
/**
* @param theCredentialString A credential string in the format <code>username:password</code>
*/
public BasicAuthInterceptor(String theCredentialString) {
Validate.notBlank(theCredentialString, "theCredentialString must not be null or blank");
Validate.isTrue(theCredentialString.contains(":"), "theCredentialString must be in the format 'username:password'");
String encoded = Base64.encodeBase64String(theCredentialString.getBytes(Constants.CHARSET_US_ASCII));
myHeaderValue = "Basic " + encoded;
}
@Override
public void interceptRequest(IHttpRequest theRequest) {
String authorizationUnescaped = StringUtils.defaultString(myUsername) + ":" + StringUtils.defaultString(myPassword);
String encoded;
try {
encoded = Base64.encodeBase64String(authorizationUnescaped.getBytes("ISO-8859-1"));
} catch (UnsupportedEncodingException e) {
throw new InternalErrorException("Could not find US-ASCII encoding. This shouldn't happen!");
}
theRequest.addHeader(Constants.HEADER_AUTHORIZATION, ("Basic " + encoded));
theRequest.addHeader(Constants.HEADER_AUTHORIZATION, myHeaderValue);
}
@Override
@ -66,6 +73,4 @@ public class BasicAuthInterceptor implements IClientInterceptor {
// nothing
}
}

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.client.interceptor;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -20,19 +20,20 @@ package ca.uhn.fhir.rest.client.interceptor;
* #L%
*/
import java.io.IOException;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import ca.uhn.fhir.rest.client.api.IClientInterceptor;
import ca.uhn.fhir.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.client.api.IHttpResponse;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import java.io.IOException;
/**
* Client interceptor which simply captures request and response objects and stores them so that they can be inspected after a client
* call has returned
*
* @see ThreadLocalCapturingInterceptor for an interceptor that uses a ThreadLocal in order to work in multithreaded environments
*/
public class CapturingInterceptor implements IClientInterceptor {
@ -63,10 +64,16 @@ public class CapturingInterceptor implements IClientInterceptor {
@Override
public void interceptResponse(IHttpResponse theResponse) {
//Buffer the reponse to avoid errors when content has already been read and the entity is not repeatable
bufferResponse(theResponse);
myLastResponse = theResponse;
}
static void bufferResponse(IHttpResponse theResponse) {
try {
if(theResponse.getResponse() instanceof HttpResponse) {
if (theResponse.getResponse() instanceof HttpResponse) {
HttpEntity entity = ((HttpResponse) theResponse.getResponse()).getEntity();
if( entity != null && !entity.isRepeatable()){
if (entity != null && !entity.isRepeatable()) {
theResponse.bufferEntity();
}
} else {
@ -75,9 +82,6 @@ public class CapturingInterceptor implements IClientInterceptor {
} catch (IOException e) {
throw new InternalErrorException("Unable to buffer the entity for capturing", e);
}
myLastResponse = theResponse;
}
}

View File

@ -0,0 +1,79 @@
package ca.uhn.fhir.rest.client.interceptor;
import ca.uhn.fhir.rest.client.api.IClientInterceptor;
import ca.uhn.fhir.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.client.api.IHttpResponse;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import java.io.IOException;
/**
* This is a client interceptor that captures the current request and response
* in a ThreadLocal variable, meaning that it can work in multithreaded
* environments without mixing up requests.
* <p>
* Use this with caution, since <b>this interceptor does not automatically clean up</b>
* the ThreadLocal after setting it. You must make sure to call
* {@link #clearThreadLocals()} after a given request has been completed,
* or you will end up leaving stale request/response objects associated
* with threads that no longer need them.
* </p>
*
* @see CapturingInterceptor for an equivalent interceptor that does not use a ThreadLocal
* @since 3.5.0
*/
public class ThreadLocalCapturingInterceptor implements IClientInterceptor {
private final ThreadLocal<IHttpRequest> myRequestThreadLocal = new ThreadLocal<>();
private final ThreadLocal<IHttpResponse> myResponseThreadLocal = new ThreadLocal<>();
private boolean myBufferResponse;
/**
* This method should be called at the end of any request process, in
* order to clear the last request and response from the current thread.
*/
public void clearThreadLocals() {
myRequestThreadLocal.remove();
myResponseThreadLocal.remove();
}
public IHttpRequest getRequestForCurrentThread() {
return myRequestThreadLocal.get();
}
public IHttpResponse getResponseForCurrentThread() {
return myResponseThreadLocal.get();
}
@Override
public void interceptRequest(IHttpRequest theRequest) {
myRequestThreadLocal.set(theRequest);
}
@Override
public void interceptResponse(IHttpResponse theResponse) {
if (isBufferResponse()) {
CapturingInterceptor.bufferResponse(theResponse);
}
myResponseThreadLocal.set(theResponse);
}
/**
* Should we buffer (capture) the response body? This defaults to
* <code>false</code>. Set to <code>true</code> if you are planning on
* examining response bodies after the response processing is complete.
*/
public boolean isBufferResponse() {
return myBufferResponse;
}
/**
* Should we buffer (capture) the response body? This defaults to
* <code>false</code>. Set to <code>true</code> if you are planning on
* examining response bodies after the response processing is complete.
*/
public ThreadLocalCapturingInterceptor setBufferResponse(boolean theBufferResponse) {
myBufferResponse = theBufferResponse;
return this;
}
}

View File

@ -108,7 +108,7 @@ public class BaseR4Config extends BaseConfig {
@Bean(name = "myResourceCountsCache")
public ResourceCountCache resourceCountsCache() {
ResourceCountCache retVal = new ResourceCountCache(() -> systemDaoR4().getResourceCounts());
retVal.setCacheMillis(60 * DateUtils.MILLIS_PER_SECOND);
retVal.setCacheMillis(10 * DateUtils.MILLIS_PER_MINUTE);
return retVal;
}

View File

@ -237,8 +237,8 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
}
@Search
public List<IBaseResource> search(
@OptionalParam(name = "_id") TokenAndListParam theIds) {
public List<IBaseResource> searchById(
@RequiredParam(name = "_id") TokenAndListParam theIds) {
List<IBaseResource> retVal = new ArrayList<>();

View File

@ -10,9 +10,7 @@ import ca.uhn.fhir.parser.CustomTypeR4Test;
import ca.uhn.fhir.parser.CustomTypeR4Test.MyCustomPatient;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.PreferReturnEnum;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException;
@ -1811,6 +1809,44 @@ public class GenericClientR4Test {
idx++;
}
@Test
public void testSearchWithParameterMap() throws Exception {
final Bundle resp1 = new Bundle();
resp1.setTotal(0);
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.getAllHeaders()).thenAnswer(new Answer<Header[]>() {
@Override
public Header[] answer(InvocationOnMock theInvocation) {
return new Header[0];
}
});
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenAnswer(t -> {
IParser p = ourCtx.newXmlParser();
return new ReaderInputStream(new StringReader(p.encodeResourceToString(resp1)), Charset.forName("UTF-8"));
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
Map<String, List<String>> rawMap = new HashMap<>();
rawMap.put("param1", Arrays.asList("val1a,val1b", "<html>"));
Bundle outcome = client
.search()
.forResource(Patient.class)
.whereMap(rawMap)
.returnBundle(Bundle.class)
.execute();
assertEquals(0, outcome.getTotal());
assertEquals("http://example.com/fhir/Patient?param1=val1a%2Cval1b&param1=%3Chtml%3E", capt.getAllValues().get(0).getURI().toASCIIString());
assertEquals("http://example.com/fhir/Patient?param1=val1a,val1b&param1=<html>", UrlUtil.unescape(capt.getAllValues().get(0).getURI().toASCIIString()));
}
/**
* See #371
*/
@ -1864,6 +1900,50 @@ public class GenericClientR4Test {
}
@Test
public void testSortUsingSortSpec() throws Exception {
final Bundle resp1 = new Bundle();
resp1.setTotal(0);
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.getAllHeaders()).thenAnswer(new Answer<Header[]>() {
@Override
public Header[] answer(InvocationOnMock theInvocation) {
return new Header[0];
}
});
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenAnswer(t -> {
IParser p = ourCtx.newXmlParser();
return new ReaderInputStream(new StringReader(p.encodeResourceToString(resp1)), Charset.forName("UTF-8"));
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
SortSpec sortSpec = new SortSpec();
sortSpec.setParamName("BDESC");
sortSpec.setOrder(SortOrderEnum.DESC);
sortSpec.setChain(new SortSpec());
sortSpec.getChain().setParamName("CASC");
sortSpec.getChain().setOrder(SortOrderEnum.ASC);
Bundle outcome = client
.search()
.forResource(Patient.class)
.returnBundle(Bundle.class)
.sort().ascending("AASC")
.sort(sortSpec)
.sort().defaultOrder("DDEF")
.execute();
assertEquals(0, outcome.getTotal());
assertEquals("http://example.com/fhir/Patient?_sort=AASC%2C-BDESC%2CCASC%2CDDEF", capt.getAllValues().get(0).getURI().toASCIIString());
assertEquals("http://example.com/fhir/Patient?_sort=AASC,-BDESC,CASC,DDEF", UrlUtil.unescape(capt.getAllValues().get(0).getURI().toASCIIString()));
}
@Test
public void testTransactionWithInvalidBody() {
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");

View File

@ -0,0 +1,174 @@
package ca.uhn.fhir.rest.client;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.client.impl.BaseClient;
import ca.uhn.fhir.rest.client.interceptor.ThreadLocalCapturingInterceptor;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.util.VersionUtil;
import javassist.tools.web.BadHttpRequest;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.ReaderInputStream;
import org.apache.http.HttpResponse;
import org.apache.http.ProtocolVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicStatusLine;
import org.hl7.fhir.r4.model.*;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.nio.charset.Charset;
import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ThreadLocalCapturingInterceptorR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ThreadLocalCapturingInterceptorR4Test.class);
private static FhirContext ourCtx;
private int myAnswerCount;
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());
myAnswerCount = 0;
System.setProperty(BaseClient.HAPI_CLIENT_KEEPRESPONSES, "true");
}
private String expectedUserAgent() {
return "HAPI-FHIR/" + VersionUtil.getVersion() + " (FHIR Client; FHIR " + FhirVersionEnum.R4.getFhirVersionString() + "/R4; apache)";
}
private byte[] extractBodyAsByteArray(ArgumentCaptor<HttpUriRequest> capt) throws IOException {
byte[] body = IOUtils.toByteArray(((HttpEntityEnclosingRequestBase) capt.getAllValues().get(0)).getEntity().getContent());
return body;
}
private String extractBodyAsString(ArgumentCaptor<HttpUriRequest> capt) throws IOException {
String body = IOUtils.toString(((HttpEntityEnclosingRequestBase) capt.getAllValues().get(0)).getEntity().getContent(), "UTF-8");
return body;
}
private ArgumentCaptor<HttpUriRequest> prepareClientForSearchResponse() throws IOException {
final String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}";
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()).then(new Answer<InputStream>() {
@Override
public InputStream answer(InvocationOnMock theInvocation) {
return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"));
}
});
return capt;
}
@Test
public void testSuccessfulSearch() throws Exception {
final String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}";
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()).then(new Answer<InputStream>() {
@Override
public InputStream answer(InvocationOnMock theInvocation) {
return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"));
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
int idx = 0;
ThreadLocalCapturingInterceptor interceptor = new ThreadLocalCapturingInterceptor();
interceptor.setBufferResponse(true);
client.registerInterceptor(interceptor);
client.setEncoding(EncodingEnum.JSON);
client.search()
.forResource("Device")
.returnBundle(Bundle.class)
.execute();
assertEquals("http://example.com/fhir/Device?_format=json", interceptor.getRequestForCurrentThread().getUri());
assertEquals(200, interceptor.getResponseForCurrentThread().getStatus());
assertEquals(msg, IOUtils.toString(interceptor.getResponseForCurrentThread().createReader()));
}
@Test
public void testFailingSearch() throws Exception {
final String msg = "BAD REQUEST";
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), 400, "Bad Request"));
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT_WITH_UTF8));
when(myHttpResponse.getEntity().getContent()).then(new Answer<InputStream>() {
@Override
public InputStream answer(InvocationOnMock theInvocation) {
return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"));
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
int idx = 0;
ThreadLocalCapturingInterceptor interceptor = new ThreadLocalCapturingInterceptor();
interceptor.setBufferResponse(true);
client.registerInterceptor(interceptor);
client.setEncoding(EncodingEnum.JSON);
try {
client.search()
.forResource("Device")
.returnBundle(Bundle.class)
.execute();
fail();
} catch (InvalidRequestException e) {
// good
}
assertEquals("http://example.com/fhir/Device?_format=json", interceptor.getRequestForCurrentThread().getUri());
assertEquals(400, interceptor.getResponseForCurrentThread().getStatus());
assertEquals(msg, IOUtils.toString(interceptor.getResponseForCurrentThread().createReader()));
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
@BeforeClass
public static void beforeClass() {
ourCtx = FhirContext.forR4();
}
}

View File

@ -159,7 +159,12 @@
</action>
<action type="add">
The HashMapResourceProvider now supports the type and
instance history operations.
instance history operations. In addition, the search method
for the
<![CDATA[<code>_id</code>]]> search parameter now has the
search parameter marked as "required". This means that additional
search methods can be added in subclasses without their intended
searches being routed to the searchById method.
</action>
<action type="fix">
Fixed a bug when creating a custom search parameter in the JPA
@ -183,6 +188,25 @@
parameter type. This is handy if you are trying to write code
that works across versions of FHIR.
</action>
<action type="add">
Several convenience methods have been added to the fluent/generic
client interfaces. These methods allow the adding of a sort via a
SortSpec object, as well as specifying search parameters via a plain
Map of Strings.
</action>
<action type="add">
A new client interceptor called ThreadLocalCapturingInterceptor has been
added. This interceptor works the same way as CapturingInterceptor in that
it captures requests and responses for later processing, but it uses
a ThreadLocal object to store them in order to facilitate
use in multithreaded environments.
</action>
<action type="add">
A new constructor has been added to the client BasicAuthInterceptor
allowing credentials to be specified in the form
"username:password" as an alternate to specifying them as two
discrete strings.
</action>
</release>
<release version="3.4.0" date="2018-05-28">
<action type="add">