Ongoing work on interceptors
This commit is contained in:
parent
23083a9283
commit
3c80238f0e
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<>();
|
||||
|
||||
|
|
|
@ -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¶m1=%3Chtml%3E", capt.getAllValues().get(0).getURI().toASCIIString());
|
||||
assertEquals("http://example.com/fhir/Patient?param1=val1a,val1b¶m1=<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");
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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">
|
||||
|
|
Loading…
Reference in New Issue