From cd8b2feb2f22b7f53c7b639bb95e00352f4fbfb0 Mon Sep 17 00:00:00 2001 From: Christian Ohr Date: Wed, 19 Dec 2018 15:33:45 +0100 Subject: [PATCH] Allow to set additional Http headers directly on the IClientExecutable --- .../fhir/rest/gclient/IClientExecutable.java | 15 ++++++ .../uhn/fhir/rest/client/impl/BaseClient.java | 15 ++++-- .../fhir/rest/client/impl/GenericClient.java | 50 ++++++++++++------- .../AdditionalRequestHeadersInterceptor.java | 7 ++- .../fhir/rest/client/GenericClientTest.java | 18 ++++++- 5 files changed, 81 insertions(+), 24 deletions(-) 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 dcf195ebb1b..afa261479e8 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 @@ -88,6 +88,21 @@ public interface IClientExecutable, Y> { */ T encodedXml(); + /** + * Set a HTTP header not explicitly defined in FHIR but commonly used in real-world scenarios. One + * important example is to set the Authorization header (e.g. Basic Auth or OAuth2-based Bearer auth), + * which tends to be cumbersome using {@link ca.uhn.fhir.rest.client.api.IClientInterceptor IClientInterceptors}, + * particularly when REST clients shall be reused and are thus supposed to remain stateless. + *

It is the responsibility of the caller to care for proper encoding of the header value, e.g. + * using Base64.

+ *

This is a short-cut alternative to using a corresponding client interceptor

+ * + * @param theHeaderName header name + * @param theHeaderValue header value + * @return + */ + T withAdditionalHeader(String theHeaderName, String theHeaderValue); + /** * Actually execute the client operation */ 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 582bda3a547..c4570048b53 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 @@ -138,7 +138,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, null, null); + return invokeClient(getFhirContext(), binding, clientInvocation, null, false, false, null, null, null, null, null); } void forceConformanceCheck() { @@ -215,11 +215,12 @@ 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, null, null); + return invokeClient(theContext, binding, clientInvocation, null, null, theLogRequestAndResponse, null, null, null, null, null); } T invokeClient(FhirContext theContext, IClientResponseHandler binding, BaseHttpClientInvocation clientInvocation, EncodingEnum theEncoding, Boolean thePrettyPrint, - boolean theLogRequestAndResponse, SummaryEnum theSummaryMode, Set theSubsetElements, CacheControlDirective theCacheControlDirective, String theCustomAcceptHeader) { + boolean theLogRequestAndResponse, SummaryEnum theSummaryMode, Set theSubsetElements, CacheControlDirective theCacheControlDirective, String theCustomAcceptHeader, + Map> theCustomHeaders) { if (!myDontValidateConformance) { myFactory.validateServerBaseIfConfiguredToDoSo(myUrlBase, myClient, this); @@ -280,6 +281,14 @@ public abstract class BaseClient implements IRestfulClient { } } + if (theCustomHeaders != null) { + for (Map.Entry> customHeader: theCustomHeaders.entrySet()) { + for (String value: customHeader.getValue()) { + httpRequest.addHeader(customHeader.getKey(), value); + } + } + } + if (theLogRequestAndResponse) { ourLog.info("Client invoking: {}", httpRequest); String body = httpRequest.getRequestBodyFromStream(); 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 3ebc89e13b9..2a3a70c8235 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 @@ -54,7 +54,6 @@ import org.hl7.fhir.instance.model.api.*; import java.io.IOException; import java.io.InputStream; -import java.io.Reader; import java.util.*; import java.util.Map.Entry; @@ -98,7 +97,8 @@ public class GenericClient extends BaseClient implements IGenericClient { } private T doReadOrVRead(final Class theType, IIdType theId, boolean theVRead, ICallable theNotModifiedHandler, String theIfVersionMatches, Boolean thePrettyPrint, - SummaryEnum theSummary, EncodingEnum theEncoding, Set theSubsetElements, String theCustomAcceptHeaderValue) { + SummaryEnum theSummary, EncodingEnum theEncoding, Set theSubsetElements, String theCustomAcceptHeaderValue, + Map> theCustomHeaders) { String resName = toResourceName(theType); IIdType id = theId; if (!id.hasBaseUrl()) { @@ -131,10 +131,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, null, theCustomAcceptHeaderValue); + return invokeClient(myContext, binding, invocation, theEncoding, thePrettyPrint, myLogRequestAndResponse, theSummary, theSubsetElements, null, theCustomAcceptHeaderValue, theCustomHeaders); } try { - return invokeClient(myContext, binding, invocation, theEncoding, thePrettyPrint, myLogRequestAndResponse, theSummary, theSubsetElements, null, theCustomAcceptHeaderValue); + return invokeClient(myContext, binding, invocation, theEncoding, thePrettyPrint, myLogRequestAndResponse, theSummary, theSubsetElements, null, theCustomAcceptHeaderValue, theCustomHeaders); } catch (NotModifiedException e) { return theNotModifiedHandler.call(); } @@ -228,7 +228,7 @@ public class GenericClient extends BaseClient implements IGenericClient { @Override public T read(final Class theType, UriDt theUrl) { IdDt id = theUrl instanceof IdDt ? ((IdDt) theUrl) : new IdDt(theUrl); - return doReadOrVRead(theType, id, false, null, null, false, null, null, null, null); + return doReadOrVRead(theType, id, false, null, null, false, null, null, null, null, null); } @Override @@ -303,10 +303,10 @@ public class GenericClient extends BaseClient implements IGenericClient { @Override public T vread(final Class theType, IdDt theId) { - if (theId.hasVersionIdPart() == false) { + if (!theId.hasVersionIdPart()) { throw new IllegalArgumentException(myContext.getLocalizer().getMessage(I18N_NO_VERSION_ID_FOR_VREAD, theId.getValue())); } - return doReadOrVRead(theType, theId, true, null, null, false, null, null, null, null); + return doReadOrVRead(theType, theId, true, null, null, false, null, null, null, null, null); } @Override @@ -327,15 +327,17 @@ public class GenericClient extends BaseClient implements IGenericClient { Boolean myPrettyPrint; SummaryEnum mySummaryMode; CacheControlDirective myCacheControlDirective; + Map> myCustomHeaderValues = new HashMap<>(); private String myCustomAcceptHeaderValue; private List> myPreferResponseTypes; private boolean myQueryLogRequestAndResponse; - private HashSet mySubsetElements; + private Set mySubsetElements; public String getCustomAcceptHeaderValue() { return myCustomAcceptHeaderValue; } + @SuppressWarnings("unchecked") @Override public T accept(String theHeaderValue) { myCustomAcceptHeaderValue = theHeaderValue; @@ -350,6 +352,7 @@ public class GenericClient extends BaseClient implements IGenericClient { return (T) this; } + @SuppressWarnings("unchecked") @Override public T cacheControl(CacheControlDirective theCacheControlDirective) { myCacheControlDirective = theCacheControlDirective; @@ -367,6 +370,7 @@ public class GenericClient extends BaseClient implements IGenericClient { return (T) this; } + @SuppressWarnings("unchecked") @Override public T encoded(EncodingEnum theEncoding) { Validate.notNull(theEncoding, "theEncoding must not be null"); @@ -388,6 +392,18 @@ public class GenericClient extends BaseClient implements IGenericClient { return (T) this; } + @SuppressWarnings("unchecked") + @Override + public T withAdditionalHeader(String theHeaderName, String theHeaderValue) { + Objects.requireNonNull(theHeaderName, "headerName cannot be null"); + Objects.requireNonNull(theHeaderValue, "headerValue cannot be null"); + if (!myCustomHeaderValues.containsKey(theHeaderName)) { + myCustomHeaderValues.put(theHeaderName, new ArrayList<>()); + } + myCustomHeaderValues.get(theHeaderName).add(theHeaderValue); + return (T) this; + } + protected EncodingEnum getParamEncoding() { return myParamEncoding; } @@ -403,7 +419,7 @@ public class GenericClient extends BaseClient implements IGenericClient { return toTypeList(theDefault); } - protected HashSet getSubsetElements() { + protected Set getSubsetElements() { return mySubsetElements; } @@ -412,7 +428,7 @@ public class GenericClient extends BaseClient implements IGenericClient { myLastRequest = theInvocation.asHttpRequest(getServerBase(), theParams, getEncoding(), myPrettyPrint); } - Z resp = invokeClient(myContext, theHandler, theInvocation, myParamEncoding, myPrettyPrint, myQueryLogRequestAndResponse || myLogRequestAndResponse, mySummaryMode, mySubsetElements, myCacheControlDirective, myCustomAcceptHeaderValue); + Z resp = invokeClient(myContext, theHandler, theInvocation, myParamEncoding, myPrettyPrint, myQueryLogRequestAndResponse || myLogRequestAndResponse, mySummaryMode, mySubsetElements, myCacheControlDirective, myCustomAcceptHeaderValue, myCustomHeaderValues); return resp; } @@ -612,7 +628,7 @@ public class GenericClient extends BaseClient implements IGenericClient { Validate.notNull(theResource, "theResource can not be null"); IIdType id = theResource.getIdElement(); Validate.notNull(id, "theResource.getIdElement() can not be null"); - if (id.hasResourceType() == false || id.hasIdPart() == false) { + if (!id.hasResourceType() || !id.hasIdPart()) { throw new IllegalArgumentException("theResource.getId() must contain a resource type and logical ID at a minimum (e.g. Patient/1234), found: " + id.getValue()); } myId = id; @@ -622,7 +638,7 @@ public class GenericClient extends BaseClient implements IGenericClient { @Override public IDeleteTyped resourceById(IIdType theId) { Validate.notNull(theId, "theId can not be null"); - if (theId.hasResourceType() == false || theId.hasIdPart() == false) { + if (!theId.hasResourceType() || !theId.hasIdPart()) { throw new IllegalArgumentException("theId must contain a resource type and logical ID at a minimum (e.g. Patient/1234)found: " + theId.getValue()); } myId = theId; @@ -773,7 +789,7 @@ public class GenericClient extends BaseClient implements IGenericClient { @Override public IHistoryUntyped onInstance(IIdType theId) { - if (theId.hasResourceType() == false) { + if (!theId.hasResourceType()) { throw new IllegalArgumentException("Resource ID does not have a resource type: " + theId.getValue()); } myId = theId; @@ -849,7 +865,7 @@ public class GenericClient extends BaseClient implements IGenericClient { continue; } String relation = ((IPrimitiveType) rel.get(0)).getValueAsString(); - if (theWantRel.equals(relation) || (theWantRel == PREVIOUS && PREV.equals(relation))) { + if (theWantRel.equals(relation) || (PREVIOUS.equals(theWantRel) && PREV.equals(relation))) { List urls = linkDef.getChildByName("url").getAccessor().getValues(nextLink); if (urls == null || urls.isEmpty()) { continue; @@ -1514,9 +1530,9 @@ public class GenericClient extends BaseClient implements IGenericClient { @Override public Object execute() {// AAA if (myId.hasVersionIdPart()) { - return doReadOrVRead(myType.getImplementingClass(), myId, true, myNotModifiedHandler, myIfVersionMatches, myPrettyPrint, mySummaryMode, myParamEncoding, getSubsetElements(), getCustomAcceptHeaderValue()); + return doReadOrVRead(myType.getImplementingClass(), myId, true, myNotModifiedHandler, myIfVersionMatches, myPrettyPrint, mySummaryMode, myParamEncoding, getSubsetElements(), getCustomAcceptHeaderValue(), myCustomHeaderValues); } - return doReadOrVRead(myType.getImplementingClass(), myId, false, myNotModifiedHandler, myIfVersionMatches, myPrettyPrint, mySummaryMode, myParamEncoding, getSubsetElements(), getCustomAcceptHeaderValue()); + return doReadOrVRead(myType.getImplementingClass(), myId, false, myNotModifiedHandler, myIfVersionMatches, myPrettyPrint, mySummaryMode, myParamEncoding, getSubsetElements(), getCustomAcceptHeaderValue(), myCustomHeaderValues); } @Override @@ -2256,7 +2272,7 @@ public class GenericClient extends BaseClient implements IGenericClient { private static void addParam(Map> params, String parameterName, String parameterValue) { if (!params.containsKey(parameterName)) { - params.put(parameterName, new ArrayList<>()); + params.put(parameterName, new ArrayList()); } params.get(parameterName).add(parameterValue); } diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/interceptor/AdditionalRequestHeadersInterceptor.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/interceptor/AdditionalRequestHeadersInterceptor.java index d359a9f3c85..bafa57e63e6 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/interceptor/AdditionalRequestHeadersInterceptor.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/interceptor/AdditionalRequestHeadersInterceptor.java @@ -33,12 +33,15 @@ import java.util.Objects; /** * This interceptor adds arbitrary header values to requests made by the client. + * + * This is now also possible directly on the Fluent Client API by calling + * {@link ca.uhn.fhir.rest.gclient.IClientExecutable#withAdditionalHeader(String, String)} */ public class AdditionalRequestHeadersInterceptor implements IClientInterceptor { private final Map> additionalHttpHeaders = new HashMap<>(); public AdditionalRequestHeadersInterceptor() { - this(new HashMap>()); + this(new HashMap<>()); } public AdditionalRequestHeadersInterceptor(Map> additionalHttpHeaders) { @@ -84,7 +87,7 @@ public class AdditionalRequestHeadersInterceptor implements IClientInterceptor { */ private List getHeaderValues(String headerName) { if (additionalHttpHeaders.get(headerName) == null) { - additionalHttpHeaders.put(headerName, new ArrayList()); + additionalHttpHeaders.put(headerName, new ArrayList<>()); } return additionalHttpHeaders.get(headerName); } 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 e69852ead58..24acacae09b 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 @@ -315,7 +315,7 @@ public class GenericClientTest { IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - MethodOutcome outcome = client.create().resource(p1).execute(); + MethodOutcome outcome = client.create().resource(p1).withAdditionalHeader("myHeaderName", "myHeaderValue").execute(); assertEquals("44", outcome.getId().getIdPart()); assertEquals("22", outcome.getId().getVersionIdPart()); @@ -325,6 +325,7 @@ public class GenericClientTest { 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()); + assertEquals("myHeaderValue", capt.getValue().getFirstHeader("myHeaderName").getValue()); count++; /* @@ -415,17 +416,20 @@ public class GenericClientTest { IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - OperationOutcome outcome = (OperationOutcome) client.delete().resourceById("Patient", "123").execute(); + OperationOutcome outcome = (OperationOutcome) client.delete().resourceById("Patient", "123") + .withAdditionalHeader("myHeaderName", "myHeaderValue").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()); + assertEquals("myHeaderValue", capt.getValue().getFirstHeader("myHeaderName").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); } @@ -455,8 +459,10 @@ public class GenericClientTest { .history() .onServer() .andReturnBundle(Bundle.class) + .withAdditionalHeader("myHeaderName", "myHeaderValue") .execute(); assertEquals("http://example.com/fhir/_history", capt.getAllValues().get(idx).getURI().toString()); + assertEquals("myHeaderValue", capt.getValue().getFirstHeader("myHeaderName").getValue()); assertEquals(1, response.getEntry().size()); idx++; @@ -464,9 +470,13 @@ public class GenericClientTest { .history() .onType(Patient.class) .andReturnBundle(Bundle.class) + .withAdditionalHeader("myHeaderName", "myHeaderValue1") + .withAdditionalHeader("myHeaderName", "myHeaderValue2") .execute(); assertEquals("http://example.com/fhir/Patient/_history", capt.getAllValues().get(idx).getURI().toString()); assertEquals(1, response.getEntry().size()); + assertEquals("myHeaderValue1", capt.getValue().getHeaders("myHeaderName")[0].getValue()); + assertEquals("myHeaderValue2", capt.getValue().getHeaders("myHeaderName")[1].getValue()); idx++; response = client @@ -1145,10 +1155,12 @@ public class GenericClientTest { .forResource(Patient.class) .withTag("urn:foo", "123") .withTag("urn:bar", "456") + .withAdditionalHeader("myHeaderName", "myHeaderValue") .returnBundle(Bundle.class) .execute(); assertEquals("http://example.com/fhir/Patient?_tag=urn%3Afoo%7C123&_tag=urn%3Abar%7C456", capt.getValue().getURI().toString()); + assertEquals("myHeaderValue", capt.getValue().getFirstHeader("myHeaderName").getValue()); } @@ -1169,10 +1181,12 @@ public class GenericClientTest { Bundle response = client.search() .forResource("Patient") .where(Patient.IDENTIFIER.exactly().systemAndCode("http://example.com/fhir", "ZZZ")) + .withAdditionalHeader("myHeaderName", "myHeaderValue") .returnBundle(Bundle.class) .execute(); assertEquals("http://example.com/fhir/Patient?identifier=http%3A%2F%2Fexample.com%2Ffhir%7CZZZ", capt.getValue().getURI().toString()); + assertEquals("myHeaderValue", capt.getValue().getFirstHeader("myHeaderName").getValue()); when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); response = client.search()