Allow client defined parameter style for _format param

This commit is contained in:
James Agnew 2018-11-08 15:27:35 -05:00
parent 3ae5f9a3b7
commit 82ec721c99
5 changed files with 241 additions and 177 deletions

View File

@ -0,0 +1,14 @@
package ca.uhn.fhir.rest.api;
public enum RequestFormatParamStyleEnum {
/**
* Do not include a _format parameter on requests
*/
NONE,
/**
* "xml" or "json"
*/
SHORT
}

View File

@ -1,5 +1,11 @@
package ca.uhn.fhir.rest.client.api;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.RequestFormatParamStyleEnum;
import ca.uhn.fhir.rest.api.SummaryEnum;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.util.List;
/*
@ -11,9 +17,9 @@ import java.util.List;
* 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,23 +28,15 @@ import java.util.List;
* #L%
*/
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.SummaryEnum;
public interface IRestfulClient {
/**
* Retrieve the contents at the given URL and parse them as a resource. This
* method could be used as a low level implementation of a read/vread/search
* operation.
*
* @param theResourceType
* The resource type to parse
* @param theUrl
* The URL to load
*
* @param theResourceType The resource type to parse
* @param theUrl The URL to load
* @return The parsed resource
*/
<T extends IBaseResource> T fetchResourceFromUrl(Class<T> theResourceType, String theUrl);
@ -49,6 +47,17 @@ public interface IRestfulClient {
*/
EncodingEnum getEncoding();
/**
* Specifies that the client should use the given encoding to do its
* queries. This means that the client will append the "_format" param
* to GET methods (read/search/etc), and will add an appropriate header for
* write methods.
*
* @param theEncoding The encoding to use in the request, or <code>null</code> not specify
* an encoding (which generally implies the use of XML). The default is <code>null</code>.
*/
void setEncoding(EncodingEnum theEncoding);
/**
* Returns the FHIR context associated with this client
*/
@ -76,25 +85,12 @@ public interface IRestfulClient {
*/
void registerInterceptor(IClientInterceptor theInterceptor);
/**
* Specifies that the client should use the given encoding to do its
* queries. This means that the client will append the "_format" param
* to GET methods (read/search/etc), and will add an appropriate header for
* write methods.
*
* @param theEncoding
* The encoding to use in the request, or <code>null</code> not specify
* an encoding (which generally implies the use of XML). The default is <code>null</code>.
*/
void setEncoding(EncodingEnum theEncoding);
/**
* Specifies that the client should request that the server respond with "pretty printing"
* enabled. Note that this is a non-standard parameter, not all servers will
* support it.
*
* @param thePrettyPrint
* The pretty print flag to use in the request (default is <code>false</code>)
*
* @param thePrettyPrint The pretty print flag to use in the request (default is <code>false</code>)
*/
void setPrettyPrint(Boolean thePrettyPrint);
@ -109,4 +105,8 @@ public interface IRestfulClient {
*/
void unregisterInterceptor(IClientInterceptor theInterceptor);
/**
* Configures what style of _format parameter should be used in requests
*/
void setFormatParamStyle(RequestFormatParamStyleEnum theRequestFormatParamStyle);
}

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.client.impl;
* 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,49 +20,11 @@ package ca.uhn.fhir.rest.client.impl;
* #L%
*/
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import ca.uhn.fhir.rest.api.CacheControlDirective;
import ca.uhn.fhir.util.XmlDetectionUtil;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.parser.DataFormatException;
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.SummaryEnum;
import ca.uhn.fhir.rest.client.api.IClientInterceptor;
import ca.uhn.fhir.rest.client.api.IHttpClient;
import ca.uhn.fhir.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.client.api.IHttpResponse;
import ca.uhn.fhir.rest.client.api.IRestfulClient;
import ca.uhn.fhir.rest.client.api.IRestfulClientFactory;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.client.api.*;
import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException;
import ca.uhn.fhir.rest.client.exceptions.InvalidResponseException;
import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException;
@ -72,7 +34,19 @@ import ca.uhn.fhir.rest.client.method.IClientResponseHandlerHandlesBinary;
import ca.uhn.fhir.rest.client.method.MethodUtil;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.util.OperationOutcomeUtil;
import ca.uhn.fhir.util.XmlUtil;
import ca.uhn.fhir.util.XmlDetectionUtil;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.util.*;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public abstract class BaseClient implements IRestfulClient {
@ -86,16 +60,17 @@ public abstract class BaseClient implements IRestfulClient {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseClient.class);
private final IHttpClient myClient;
private final RestfulClientFactory myFactory;
private final String myUrlBase;
private boolean myDontValidateConformance;
private EncodingEnum myEncoding = null; // default unspecified (will be XML)
private final RestfulClientFactory myFactory;
private List<IClientInterceptor> myInterceptors = new ArrayList<IClientInterceptor>();
private boolean myKeepResponses = false;
private IHttpResponse myLastResponse;
private String myLastResponseBody;
private Boolean myPrettyPrint = false;
private SummaryEnum mySummary;
private final String myUrlBase;
private RequestFormatParamStyleEnum myRequestFormatParamStyle = RequestFormatParamStyleEnum.SHORT;
BaseClient(IHttpClient theClient, String theUrlBase, RestfulClientFactory theFactory) {
super();
@ -121,10 +96,12 @@ public abstract class BaseClient implements IRestfulClient {
protected Map<String, List<String>> createExtraParams() {
HashMap<String, List<String>> retVal = new LinkedHashMap<String, List<String>>();
if (getEncoding() == EncodingEnum.XML) {
retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("xml"));
} else if (getEncoding() == EncodingEnum.JSON) {
retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("json"));
if (myRequestFormatParamStyle == RequestFormatParamStyleEnum.SHORT) {
if (getEncoding() == EncodingEnum.XML) {
retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("xml"));
} else if (getEncoding() == EncodingEnum.JSON) {
retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("json"));
}
}
if (isPrettyPrint()) {
@ -150,6 +127,17 @@ public abstract class BaseClient implements IRestfulClient {
return myEncoding;
}
/**
* Sets the encoding that will be used on requests. Default is <code>null</code>, which means the client will not
* explicitly request an encoding. (This is perfectly acceptable behaviour according to the FHIR specification. In
* this case, the server will choose which encoding to return, and the client can handle either XML or JSON)
*/
@Override
public void setEncoding(EncodingEnum theEncoding) {
myEncoding = theEncoding;
// return this;
}
/**
* {@inheritDoc}
*/
@ -192,10 +180,21 @@ public abstract class BaseClient implements IRestfulClient {
return mySummary;
}
@Override
public void setSummary(SummaryEnum theSummary) {
mySummary = theSummary;
}
public String getUrlBase() {
return myUrlBase;
}
@Override
public void setFormatParamStyle(RequestFormatParamStyleEnum theRequestFormatParamStyle) {
Validate.notNull(theRequestFormatParamStyle, "theRequestFormatParamStyle must not be null");
myRequestFormatParamStyle = theRequestFormatParamStyle;
}
<T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation) {
return invokeClient(theContext, binding, clientInvocation, false);
}
@ -219,10 +218,12 @@ public abstract class BaseClient implements IRestfulClient {
Map<String, List<String>> params = createExtraParams();
if (clientInvocation instanceof HttpGetClientInvocation) {
if (theEncoding == EncodingEnum.XML) {
params.put(Constants.PARAM_FORMAT, Collections.singletonList("xml"));
} else if (theEncoding == EncodingEnum.JSON) {
params.put(Constants.PARAM_FORMAT, Collections.singletonList("json"));
if (myRequestFormatParamStyle == RequestFormatParamStyleEnum.SHORT) {
if (theEncoding == EncodingEnum.XML) {
params.put(Constants.PARAM_FORMAT, Collections.singletonList("xml"));
} else if (theEncoding == EncodingEnum.JSON) {
params.put(Constants.PARAM_FORMAT, Collections.singletonList("json"));
}
}
}
@ -252,7 +253,7 @@ public abstract class BaseClient implements IRestfulClient {
addToCacheControlHeader(b, Constants.CACHE_CONTROL_NO_CACHE, theCacheControlDirective.isNoCache());
addToCacheControlHeader(b, Constants.CACHE_CONTROL_NO_STORE, theCacheControlDirective.isNoStore());
if (theCacheControlDirective.getMaxResults() != null) {
addToCacheControlHeader(b, Constants.CACHE_CONTROL_MAX_RESULTS+"="+ Integer.toString(theCacheControlDirective.getMaxResults().intValue()), true);
addToCacheControlHeader(b, Constants.CACHE_CONTROL_MAX_RESULTS + "=" + Integer.toString(theCacheControlDirective.getMaxResults().intValue()), true);
}
if (b.length() > 0) {
httpRequest.addHeader(Constants.HEADER_CACHE_CONTROL, b.toString());
@ -397,6 +398,13 @@ public abstract class BaseClient implements IRestfulClient {
return myKeepResponses;
}
/**
* For now, this is a part of the internal API of HAPI - Use with caution as this method may change!
*/
public void setKeepResponses(boolean theKeepResponses) {
myKeepResponses = theKeepResponses;
}
/**
* Returns the pretty print flag, which is a request to the server for it to return "pretty printed" responses. Note
* that this is currently a non-standard flag (_pretty) which is supported only by HAPI based servers (and any other
@ -406,6 +414,17 @@ public abstract class BaseClient implements IRestfulClient {
return Boolean.TRUE.equals(myPrettyPrint);
}
/**
* Sets the pretty print flag, which is a request to the server for it to return "pretty printed" responses. Note
* that this is currently a non-standard flag (_pretty) which is supported only by HAPI based servers (and any other
* servers which might implement it).
*/
@Override
public void setPrettyPrint(Boolean thePrettyPrint) {
myPrettyPrint = thePrettyPrint;
// return this;
}
private void keepResponseAndLogIt(boolean theLogRequestAndResponse, IHttpResponse response, String responseString) {
if (myKeepResponses) {
myLastResponse = response;
@ -438,55 +457,12 @@ public abstract class BaseClient implements IRestfulClient {
myDontValidateConformance = theDontValidateConformance;
}
/**
* Sets the encoding that will be used on requests. Default is <code>null</code>, which means the client will not
* explicitly request an encoding. (This is perfectly acceptable behaviour according to the FHIR specification. In
* this case, the server will choose which encoding to return, and the client can handle either XML or JSON)
*/
@Override
public void setEncoding(EncodingEnum theEncoding) {
myEncoding = theEncoding;
// return this;
}
/**
* For now, this is a part of the internal API of HAPI - Use with caution as this method may change!
*/
public void setKeepResponses(boolean theKeepResponses) {
myKeepResponses = theKeepResponses;
}
/**
* Sets the pretty print flag, which is a request to the server for it to return "pretty printed" responses. Note
* that this is currently a non-standard flag (_pretty) which is supported only by HAPI based servers (and any other
* servers which might implement it).
*/
@Override
public void setPrettyPrint(Boolean thePrettyPrint) {
myPrettyPrint = thePrettyPrint;
// return this;
}
@Override
public void setSummary(SummaryEnum theSummary) {
mySummary = theSummary;
}
@Override
public void unregisterInterceptor(IClientInterceptor theInterceptor) {
Validate.notNull(theInterceptor, "Interceptor can not be null");
myInterceptors.remove(theInterceptor);
}
static ArrayList<Class<? extends IBaseResource>> toTypeList(Class<? extends IBaseResource> thePreferResponseType) {
ArrayList<Class<? extends IBaseResource>> preferResponseTypes = null;
if (thePreferResponseType != null) {
preferResponseTypes = new ArrayList<Class<? extends IBaseResource>>(1);
preferResponseTypes.add(thePreferResponseType);
}
return preferResponseTypes;
}
protected final class ResourceResponseHandler<T extends IBaseResource> implements IClientResponseHandler<T> {
private boolean myAllowHtmlResponse;
@ -568,4 +544,13 @@ public abstract class BaseClient implements IRestfulClient {
}
}
static ArrayList<Class<? extends IBaseResource>> toTypeList(Class<? extends IBaseResource> thePreferResponseType) {
ArrayList<Class<? extends IBaseResource>> preferResponseTypes = null;
if (thePreferResponseType != null) {
preferResponseTypes = new ArrayList<Class<? extends IBaseResource>>(1);
preferResponseTypes.add(thePreferResponseType);
}
return preferResponseTypes;
}
}

View File

@ -1,17 +1,13 @@
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.MethodOutcome;
import ca.uhn.fhir.rest.api.RequestFormatParamStyleEnum;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.util.RandomServerPortProvider;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.util.VersionUtil;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
@ -20,9 +16,7 @@ import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ -40,8 +34,9 @@ public class ClientHeadersR4Test {
private static Server ourServer;
private static String ourServerBase;
private static HashMap<String, List<String>> ourHeaders;
private static IGenericClient ourClient;
private static HashMap<String, String[]> ourParams;
private static String ourMethod;
private IGenericClient myClient;
@Before
public void before() {
@ -49,34 +44,125 @@ public class ClientHeadersR4Test {
ourMethod = null;
}
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;
}
@Test
public void testCreateWithPreferRepresentationServerReturnsResource() throws Exception {
public void testReadXml() {
myClient
.read()
.resource("Patient")
.withId(123L)
.encodedXml()
.execute();
assertEquals("application/fhir+xml;q=1.0, application/xml+fhir;q=0.9", ourHeaders.get(Constants.HEADER_ACCEPT).get(0));
assertEquals("xml", ourParams.get(Constants.PARAM_FORMAT)[0]);
}
@Test
public void testReadXmlNoParam() {
myClient.setFormatParamStyle(RequestFormatParamStyleEnum.NONE);
myClient
.read()
.resource("Patient")
.withId(123L)
.encodedXml()
.execute();
assertEquals("application/fhir+xml;q=1.0, application/xml+fhir;q=0.9", ourHeaders.get(Constants.HEADER_ACCEPT).get(0));
assertEquals(null, ourParams.get(Constants.PARAM_FORMAT));
}
@Test
public void testReadJson() {
myClient
.read()
.resource("Patient")
.withId(123L)
.encodedJson()
.execute();
assertEquals("application/fhir+json;q=1.0, application/json+fhir;q=0.9", ourHeaders.get(Constants.HEADER_ACCEPT).get(0));
assertEquals("json", ourParams.get(Constants.PARAM_FORMAT)[0]);
}
@Test
public void testReadJsonNoParam() {
myClient.setFormatParamStyle(RequestFormatParamStyleEnum.NONE);
myClient
.read()
.resource("Patient")
.withId(123L)
.encodedJson()
.execute();
assertEquals("application/fhir+json;q=1.0, application/json+fhir;q=0.9", ourHeaders.get(Constants.HEADER_ACCEPT).get(0));
assertEquals(null, ourParams.get(Constants.PARAM_FORMAT));
}
@Test
public void testReadXmlDisable() {
myClient
.read()
.resource("Patient")
.withId(123L)
.encodedXml()
.execute();
assertEquals("application/fhir+xml;q=1.0, application/xml+fhir;q=0.9", ourHeaders.get(Constants.HEADER_ACCEPT).get(0));
assertEquals("xml", ourParams.get(Constants.PARAM_FORMAT)[0]);
}
@Test
public void testCreateWithPreferRepresentationServerReturnsResource() {
final Patient resp1 = new Patient();
resp1.setActive(true);
MethodOutcome resp = ourClient.create().resource(resp1).execute();
MethodOutcome resp = myClient.create().resource(resp1).execute();
assertNotNull(resp);
assertEquals(1, ourHeaders.get(Constants.HEADER_CONTENT_TYPE).size());
assertEquals("application/fhir+xml; charset=UTF-8", ourHeaders.get(Constants.HEADER_CONTENT_TYPE).get(0));
}
@Before
public void beforeCreateClient() {
myClient = ourCtx.newRestfulGenericClient(ourServerBase);
}
private static class TestServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
if (ourHeaders != null) {
fail();
}
ourHeaders = new HashMap<>();
ourParams = new HashMap<>(req.getParameterMap());
ourMethod = req.getMethod();
Enumeration<String> names = req.getHeaderNames();
while (names.hasMoreElements()) {
String nextName = names.nextElement();
ourHeaders.put(nextName, new ArrayList<>());
Enumeration<String> values = req.getHeaders(nextName);
while (values.hasMoreElements()) {
ourHeaders.get(nextName).add(values.nextElement());
}
}
resp.setStatus(200);
if (req.getMethod().equals("GET")) {
resp.setContentType("application/json");
resp.getWriter().append("{\"resourceType\":\"Patient\"}");
resp.getWriter().close();
}
}
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
@ -94,7 +180,6 @@ public class ClientHeadersR4Test {
ourServerBase = "http://localhost:" + myPort + "/fhir/context";
ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
ourClient = ourCtx.newRestfulGenericClient(ourServerBase);
ServletHolder servletHolder = new ServletHolder();
servletHolder.setServlet(new TestServlet());
@ -105,29 +190,4 @@ public class ClientHeadersR4Test {
}
private static class TestServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
if (ourHeaders != null) {
fail();
}
ourHeaders = new HashMap<>();
ourMethod = req.getMethod();
Enumeration<String> names = req.getHeaderNames();
while (names.hasMoreElements()) {
String nextName = names.nextElement();
ourHeaders.put(nextName, new ArrayList<String>());
Enumeration<String> values = req.getHeaders(nextName);
while (values.hasMoreElements()) {
ourHeaders.get(nextName).add(values.nextElement());
}
}
resp.setStatus(200);
}
}
}

View File

@ -171,7 +171,12 @@
<action type="fix" issue="1071" dev="volsch">
When restful reponses tried to return multiple instances of the same response header,
some instances were discarded. Thanks to Volker Schmidt for the pull request!
</action>"
</action>
<action type="add">
The REST client now allows for configurable behaviour as to whether a
<![CDATA[<code>_format</code>]]>
parameter should be included in requests.
</action>
</release>
<release version="3.5.0" date="2018-09-17">