Allow client to return methodoutcome instead of resdource for operation

call
This commit is contained in:
James Agnew 2018-11-19 11:11:49 +01:00
parent 67dbc802be
commit 58388bb614
22 changed files with 669 additions and 344 deletions

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.parser;
* 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.
@ -27,6 +27,7 @@ import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.UrlUtil;
import com.google.common.base.Charsets;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.*;
@ -623,6 +624,16 @@ public abstract class BaseParser implements IParser {
return mySuppressNarratives;
}
@Override
public IBaseResource parseResource(InputStream theInputStream) throws DataFormatException {
return parseResource(new InputStreamReader(theInputStream, Charsets.UTF_8));
}
@Override
public <T extends IBaseResource> T parseResource(Class<T> theResourceType, InputStream theInputStream) throws DataFormatException {
return parseResource(theResourceType, new InputStreamReader(theInputStream, Charsets.UTF_8));
}
@Override
public <T extends IBaseResource> T parseResource(Class<T> theResourceType, Reader theReader) throws DataFormatException {

View File

@ -19,14 +19,23 @@ package ca.uhn.fhir.parser;
* limitations under the License.
* #L%
*/
import java.io.*;
import java.util.*;
import org.hl7.fhir.instance.model.api.*;
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.ParserOptions;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.rest.api.EncodingEnum;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.Writer;
import java.util.Collection;
import java.util.List;
import java.util.Set;
/**
* A parser, which can be used to convert between HAPI FHIR model/structure objects, and their respective String wire
@ -127,6 +136,20 @@ public interface IParser {
*/
<T extends IBaseResource> T parseResource(Class<T> theResourceType, Reader theReader) throws DataFormatException;
/**
* Parses a resource
*
* @param theResourceType
* The resource type to use. This can be used to explicitly specify a class which extends a built-in type
* (e.g. a custom type extending the default Patient class)
* @param theInputStream
* The InputStream to parse input from, <b>with an implied charset of UTF-8</b>. Note that the InputStream will not be closed by the parser upon completion.
* @return A parsed resource
* @throws DataFormatException
* If the resource can not be parsed because the data is not recognized or invalid for any reason
*/
<T extends IBaseResource> T parseResource(Class<T> theResourceType, InputStream theInputStream) throws DataFormatException;
/**
* Parses a resource
*
@ -153,6 +176,19 @@ public interface IParser {
*/
IBaseResource parseResource(Reader theReader) throws ConfigurationException, DataFormatException;
/**
* Parses a resource
*
* @param theInputStream
* The InputStream to parse input from (charset is assumed to be UTF-8).
* Note that the stream will not be closed by the parser upon completion.
* @return A parsed resource. Note that the returned object will be an instance of {@link IResource} or
* {@link IAnyResource} depending on the specific FhirContext which created this parser.
* @throws DataFormatException
* If the resource can not be parsed because the data is not recognized or invalid for any reason
*/
IBaseResource parseResource(InputStream theInputStream) throws ConfigurationException, DataFormatException;
/**
* Parses a resource
*

View File

@ -42,8 +42,14 @@ public class Constants {
*/
public static final Set<String> CORS_ALLWED_METHODS;
public static final String CT_FHIR_JSON = "application/json+fhir";
/**
* The FHIR MimeType for JSON encoding in FHIR DSTU3+
*/
public static final String CT_FHIR_JSON_NEW = "application/fhir+json";
public static final String CT_FHIR_XML = "application/xml+fhir";
/**
* The FHIR MimeType for XML encoding in FHIR DSTU3+
*/
public static final String CT_FHIR_XML_NEW = "application/fhir+xml";
public static final String CT_HTML = "text/html";
public static final String CT_HTML_WITH_UTF8 = "text/html" + CHARSET_UTF8_CTSUFFIX;
@ -86,6 +92,7 @@ public class Constants {
public static final String HEADER_CONTENT_LOCATION = "Content-Location";
public static final String HEADER_CONTENT_LOCATION_LC = HEADER_CONTENT_LOCATION.toLowerCase();
public static final String HEADER_CONTENT_TYPE = "Content-Type";
public static final String HEADER_CONTENT_TYPE_LC = HEADER_CONTENT_TYPE.toLowerCase();
public static final String HEADER_COOKIE = "Cookie";
public static final String HEADER_CORS_ALLOW_METHODS = "Access-Control-Allow-Methods";
public static final String HEADER_CORS_ALLOW_ORIGIN = "Access-Control-Allow-Origin";

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.api;
* 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,11 +20,13 @@ package ca.uhn.fhir.rest.api;
* #L%
*/
import ca.uhn.fhir.util.CoverageIgnore;
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 ca.uhn.fhir.util.CoverageIgnore;
import java.util.List;
import java.util.Map;
public class MethodOutcome {
@ -32,6 +34,7 @@ public class MethodOutcome {
private IIdType myId;
private IBaseOperationOutcome myOperationOutcome;
private IBaseResource myResource;
private Map<String, List<String>> myResponseHeaders;
/**
* Constructor
@ -42,13 +45,10 @@ public class MethodOutcome {
/**
* Constructor
*
* @param theId
* The ID of the created/updated resource
*
* @param theCreated
* If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called
* whether the result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist.
*
* @param theId The ID of the created/updated resource
* @param theCreated If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called
* whether the result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist.
*/
@CoverageIgnore
public MethodOutcome(IIdType theId, Boolean theCreated) {
@ -58,12 +58,9 @@ public class MethodOutcome {
/**
* Constructor
*
* @param theId
* The ID of the created/updated resource
*
* @param theBaseOperationOutcome
* The operation outcome to return with the response (or null for none)
*
* @param theId The ID of the created/updated resource
* @param theBaseOperationOutcome The operation outcome to return with the response (or null for none)
*/
public MethodOutcome(IIdType theId, IBaseOperationOutcome theBaseOperationOutcome) {
myId = theId;
@ -72,16 +69,11 @@ public class MethodOutcome {
/**
* Constructor
*
* @param theId
* The ID of the created/updated resource
*
* @param theBaseOperationOutcome
* The operation outcome to return with the response (or null for none)
*
* @param theCreated
* If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called
* whether the result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist.
*
* @param theId The ID of the created/updated resource
* @param theBaseOperationOutcome The operation outcome to return with the response (or null for none)
* @param theCreated If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called
* whether the result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist.
*/
public MethodOutcome(IIdType theId, IBaseOperationOutcome theBaseOperationOutcome, Boolean theCreated) {
myId = theId;
@ -91,9 +83,8 @@ public class MethodOutcome {
/**
* Constructor
*
* @param theId
* The ID of the created/updated resource
*
* @param theId The ID of the created/updated resource
*/
public MethodOutcome(IIdType theId) {
myId = theId;
@ -101,9 +92,8 @@ public class MethodOutcome {
/**
* Constructor
*
* @param theOperationOutcome
* The operation outcome resource to return
*
* @param theOperationOutcome The operation outcome resource to return
*/
public MethodOutcome(IBaseOperationOutcome theOperationOutcome) {
myOperationOutcome = theOperationOutcome;
@ -117,19 +107,54 @@ public class MethodOutcome {
return myCreated;
}
/**
* If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called whether the
* result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist.
* <p>
* Users of HAPI should only interact with this method in Server applications
* </p>
*
* @param theCreated If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called
* whether the result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist.
* @return Returns a reference to <code>this</code> for easy method chaining
*/
public MethodOutcome setCreated(Boolean theCreated) {
myCreated = theCreated;
return this;
}
public IIdType getId() {
return myId;
}
/**
* @param theId The ID of the created/updated resource
* @return Returns a reference to <code>this</code> for easy method chaining
*/
public MethodOutcome setId(IIdType theId) {
myId = theId;
return this;
}
/**
* Returns the {@link IBaseOperationOutcome} resource to return to the client or <code>null</code> if none.
*
*
* @return This method <b>will return null</b>, unlike many methods in the API.
*/
public IBaseOperationOutcome getOperationOutcome() {
return myOperationOutcome;
}
/**
* Sets the {@link IBaseOperationOutcome} resource to return to the client. Set to <code>null</code> (which is the default) if none.
*
* @return Returns a reference to <code>this</code> for easy method chaining
*/
public MethodOutcome setOperationOutcome(IBaseOperationOutcome theBaseOperationOutcome) {
myOperationOutcome = theBaseOperationOutcome;
return this;
}
/**
* <b>From a client response:</b> If the method returned an actual resource body (e.g. a create/update with
* "Prefer: return=representation") this field will be populated with the
@ -139,50 +164,15 @@ public class MethodOutcome {
return myResource;
}
/**
* If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called whether the
* result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist.
* <p>
* Users of HAPI should only interact with this method in Server applications
* </p>
*
* @param theCreated
* If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called
* whether the result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist.
* @return Returns a reference to <code>this</code> for easy method chaining
*/
public MethodOutcome setCreated(Boolean theCreated) {
myCreated = theCreated;
return this;
}
/**
* @param theId
* The ID of the created/updated resource
* @return Returns a reference to <code>this</code> for easy method chaining
*/
public MethodOutcome setId(IIdType theId) {
myId = theId;
return this;
}
/**
* Sets the {@link IBaseOperationOutcome} resource to return to the client. Set to <code>null</code> (which is the default) if none.
* @return Returns a reference to <code>this</code> for easy method chaining
*/
public MethodOutcome setOperationOutcome(IBaseOperationOutcome theBaseOperationOutcome) {
myOperationOutcome = theBaseOperationOutcome;
return this;
}
/**
* <b>In a server response</b>: This field may be populated in server code with the final resource for operations
* where a resource body is being created/updated. E.g. for an update method, this field could be populated with
* the resource after the update is applied, with the new version ID, lastUpdate time, etc.
* the resource after the update is applied, with the new version ID, lastUpdate time, etc.
* <p>
* This field is optional, but if it is populated the server will return the resource body if requested to
* do so via the HTTP Prefer header.
* </p>
* </p>
*
* @return Returns a reference to <code>this</code> for easy method chaining
*/
public MethodOutcome setResource(IBaseResource theResource) {
@ -190,4 +180,23 @@ public class MethodOutcome {
return this;
}
/**
* Gets the headers for the HTTP response
*/
public Map<String, List<String>> getResponseHeaders() {
return myResponseHeaders;
}
/**
* Sets the headers for the HTTP response
*/
public void setResponseHeaders(Map<String, List<String>> theResponseHeaders) {
myResponseHeaders = theResponseHeaders;
}
public void setCreatedUsingStatusCode(int theResponseStatusCode) {
if (theResponseStatusCode == Constants.STATUS_HTTP_201_CREATED) {
setCreated(true);
}
}
}

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.client.api;
* 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.
@ -28,16 +28,18 @@ import java.util.Map;
* Http Request. Allows addition of headers and execution of the request.
*/
public interface IHttpRequest {
/**
* Add a header to the request
* @param theName the header name
*
* @param theName the header name
* @param theValue the header value
*/
void addHeader(String theName, String theValue);
/**
* Execute the request
*
* @return the response
*/
IHttpResponse execute() throws IOException;
@ -50,7 +52,8 @@ public interface IHttpRequest {
/**
* Return the request body as a string.
* If this is not supported by the underlying technology, null is returned
* If this is not supported by the underlying technology, null is returned
*
* @return a string representation of the request or null if not supported or empty.
*/
String getRequestBodyFromStream() throws IOException;
@ -59,10 +62,16 @@ public interface IHttpRequest {
* Return the request URI, or null
*/
String getUri();
/**
* Return the HTTP verb (e.g. "GET")
*/
String getHttpVerbName();
/**
* Remove any headers matching the given name
*
* @param theHeaderName The header name, e.g. "Accept" (must not be null or blank)
*/
void removeHeaders(String theHeaderName);
}

View File

@ -70,7 +70,7 @@ public interface IHttpResponse {
void close();
/**
* Returna reader for the response entity
* Returns a reader for the response entity
*/
Reader createReader() throws IOException;

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.client.exceptions;
* 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.
@ -19,15 +19,18 @@ package ca.uhn.fhir.rest.client.exceptions;
* limitations under the License.
* #L%
*/
import static org.apache.commons.lang3.StringUtils.isBlank;
import java.io.IOException;
import java.io.Reader;
import org.apache.commons.io.IOUtils;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.util.CoverageIgnore;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import static org.apache.commons.lang3.StringUtils.isBlank;
@CoverageIgnore
public class NonFhirResponseException extends BaseServerResponseException {
@ -36,24 +39,30 @@ public class NonFhirResponseException extends BaseServerResponseException {
/**
* Constructor
*
* @param theMessage
* The message
* @param theResponseText
* @param theStatusCode
* @param theResponseReader
* @param theContentType
*
* @param theMessage The message
* @param theStatusCode The HTTP status code
*/
NonFhirResponseException(int theStatusCode, String theMessage) {
super(theStatusCode, theMessage);
}
public static NonFhirResponseException newInstance(int theStatusCode, String theContentType, InputStream theInputStream) {
return newInstance(theStatusCode, theContentType, new InputStreamReader(theInputStream, Charsets.UTF_8));
}
public static NonFhirResponseException newInstance(int theStatusCode, String theContentType, Reader theReader) {
String responseBody = "";
try {
responseBody = IOUtils.toString(theReader);
} catch (IOException e) {
IOUtils.closeQuietly(theReader);
// ignore
} finally {
try {
theReader.close();
} catch (IOException theE) {
// ignore
}
}
NonFhirResponseException retVal;

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.rest.gclient;
import ca.uhn.fhir.rest.api.CacheControlDirective;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.RequestFormatParamStyleEnum;
import ca.uhn.fhir.rest.api.SummaryEnum;
import org.hl7.fhir.instance.model.api.IBaseResource;
@ -16,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.
@ -28,12 +29,12 @@ import java.util.List;
*/
public interface IClientExecutable<T extends IClientExecutable<?,Y>, Y> {
public interface IClientExecutable<T extends IClientExecutable<?, Y>, Y> {
/**
* If set to true, the client will log the request and response to the SLF4J logger. This can be useful for
* debugging, but is generally not desirable in a production situation.
*
*
* @deprecated Use the client logging interceptor to log requests and responses instead. See <a href="http://jamesagnew.github.io/hapi-fhir/doc_rest_client.html#req_resp_logging">here</a> for more information.
*/
@Deprecated
@ -46,16 +47,45 @@ public interface IClientExecutable<T extends IClientExecutable<?,Y>, Y> {
T cacheControl(CacheControlDirective theCacheControlDirective);
/**
* Request that the server return subsetted resources, containing only the elements specified in the given parameters.
* Request that the server return subsetted resources, containing only the elements specified in the given parameters.
* For example: <code>subsetElements("name", "identifier")</code> requests that the server only return
* the "name" and "identifier" fields in the returned resource, and omit any others.
* the "name" and "identifier" fields in the returned resource, and omit any others.
*/
T elementsSubset(String... theElements);
/**
* Request that the server respond with JSON via the Accept header and possibly also the
* <code>_format</code> parameter if {@link ca.uhn.fhir.rest.client.api.IRestfulClient#setFormatParamStyle(RequestFormatParamStyleEnum) configured to do so}.
* <p>
* This method will have no effect if {@link #accept(String) a custom Accept header} is specified.
* </p>
*
* @see #accept(String)
*/
T encoded(EncodingEnum theEncoding);
/**
* Request that the server respond with JSON via the Accept header and possibly also the
* <code>_format</code> parameter if {@link ca.uhn.fhir.rest.client.api.IRestfulClient#setFormatParamStyle(RequestFormatParamStyleEnum) configured to do so}.
* <p>
* This method will have no effect if {@link #accept(String) a custom Accept header} is specified.
* </p>
*
* @see #accept(String)
* @see #encoded(EncodingEnum)
*/
T encodedJson();
/**
* Request that the server respond with JSON via the Accept header and possibly also the
* <code>_format</code> parameter if {@link ca.uhn.fhir.rest.client.api.IRestfulClient#setFormatParamStyle(RequestFormatParamStyleEnum) configured to do so}.
* <p>
* This method will have no effect if {@link #accept(String) a custom Accept header} is specified.
* </p>
*
* @see #accept(String)
* @see #encoded(EncodingEnum)
*/
T encodedXml();
/**
@ -84,11 +114,33 @@ public interface IClientExecutable<T extends IClientExecutable<?,Y>, Y> {
*/
T preferResponseTypes(List<Class<? extends IBaseResource>> theTypes);
/**
* Request pretty-printed response via the <code>_pretty</code> parameter
*/
T prettyPrint();
/**
* Request that the server modify the response using the <code>_summary</code> param
* Request that the server modify the response using the <code>_summary</code> param
*/
T summaryMode(SummaryEnum theSummary);
/**
* Specifies a custom <code>Accept</code> header that should be supplied with the
* request.
* <p>
* Note that this method overrides any encoding preferences specified with
* {@link #encodedJson()} or {@link #encodedXml()}. It is generally easier to
* just use those methods if you simply want to request a specific FHIR encoding.
* </p>
*
* @param theHeaderValue The header value, e.g. "application/fhir+json". Constants such
* as {@link ca.uhn.fhir.rest.api.Constants#CT_FHIR_XML_NEW} and
* {@link ca.uhn.fhir.rest.api.Constants#CT_FHIR_JSON_NEW} may
* be useful. If set to <code>null</code> or an empty string, the
* default Accept header will be used.
* @see #encoded(EncodingEnum)
* @see #encodedJson()
* @see #encodedXml()
*/
T accept(String theHeaderValue);
}

View File

@ -20,6 +20,7 @@ package ca.uhn.fhir.rest.gclient;
* #L%
*/
import ca.uhn.fhir.rest.api.MethodOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource;
public interface IOperationUntypedWithInput<T> extends IClientExecutable<IOperationUntypedWithInput<T>, T> {
@ -43,4 +44,9 @@ public interface IOperationUntypedWithInput<T> extends IClientExecutable<IOperat
*/
<R extends IBaseResource> IOperationUntypedWithInput<R> returnResourceType(Class<R> theReturnType);
/**
* Request that the method chain returns a {@link MethodOutcome} object. This object
* will contain details
*/
IOperationUntypedWithInput<MethodOutcome> returnMethodOutcome();
}

View File

@ -20,12 +20,11 @@ package ca.uhn.fhir.rest.client.apache;
* #L%
*/
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.*;
import ca.uhn.fhir.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.client.api.IHttpResponse;
import ca.uhn.fhir.util.StopWatch;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.Validate;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
@ -34,13 +33,14 @@ import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.ContentType;
import ca.uhn.fhir.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.client.api.IHttpResponse;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.*;
/**
* A Http Request based on Apache. This is an adapter around the class
* {@link org.apache.http.client.methods.HttpRequestBase HttpRequestBase}
*
*
* @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare
*/
public class ApacheHttpRequest implements IHttpRequest {
@ -79,6 +79,7 @@ public class ApacheHttpRequest implements IHttpRequest {
/**
* Get the ApacheRequest
*
* @return the ApacheRequest
*/
public HttpRequestBase getApacheRequest() {
@ -90,6 +91,12 @@ public class ApacheHttpRequest implements IHttpRequest {
return myRequest.getMethod();
}
@Override
public void removeHeaders(String theHeaderName) {
Validate.notBlank(theHeaderName, "theHeaderName must not be null or blank");
myRequest.removeHeaders(theHeaderName);
}
@Override
public String getRequestBodyFromStream() throws IOException {
if (myRequest instanceof HttpEntityEnclosingRequest) {

View File

@ -33,19 +33,20 @@ import ca.uhn.fhir.rest.client.method.IClientResponseHandler;
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.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.BinaryUtil;
import ca.uhn.fhir.util.OperationOutcomeUtil;
import ca.uhn.fhir.util.XmlDetectionUtil;
import com.google.common.base.Charsets;
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.io.*;
import java.util.*;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public abstract class BaseClient implements IRestfulClient {
@ -93,14 +94,16 @@ public abstract class BaseClient implements IRestfulClient {
}
protected Map<String, List<String>> createExtraParams() {
HashMap<String, List<String>> retVal = new LinkedHashMap<String, List<String>>();
protected Map<String, List<String>> createExtraParams(String theCustomAcceptHeader) {
HashMap<String, List<String>> retVal = new LinkedHashMap<>();
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 (isBlank(theCustomAcceptHeader)) {
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"));
}
}
}
@ -115,7 +118,7 @@ public abstract class BaseClient implements IRestfulClient {
public <T extends IBaseResource> T fetchResourceFromUrl(Class<T> theResourceType, String theUrl) {
BaseHttpClientInvocation clientInvocation = new HttpGetClientInvocation(getFhirContext(), theUrl);
ResourceResponseHandler<T> binding = new ResourceResponseHandler<T>(theResourceType);
return invokeClient(getFhirContext(), binding, clientInvocation, null, false, false, null, null, null);
return invokeClient(getFhirContext(), binding, clientInvocation, null, false, false, null, null, null, null);
}
void forceConformanceCheck() {
@ -200,11 +203,11 @@ public abstract class BaseClient implements IRestfulClient {
}
<T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation, boolean theLogRequestAndResponse) {
return invokeClient(theContext, binding, clientInvocation, null, null, theLogRequestAndResponse, null, null, null);
return invokeClient(theContext, binding, clientInvocation, null, null, theLogRequestAndResponse, null, null, null, null);
}
<T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation, EncodingEnum theEncoding, Boolean thePrettyPrint,
boolean theLogRequestAndResponse, SummaryEnum theSummaryMode, Set<String> theSubsetElements, CacheControlDirective theCacheControlDirective) {
boolean theLogRequestAndResponse, SummaryEnum theSummaryMode, Set<String> theSubsetElements, CacheControlDirective theCacheControlDirective, String theCustomAcceptHeader) {
if (!myDontValidateConformance) {
myFactory.validateServerBaseIfConfiguredToDoSo(myUrlBase, myClient, this);
@ -215,10 +218,10 @@ public abstract class BaseClient implements IRestfulClient {
IHttpRequest httpRequest = null;
IHttpResponse response = null;
try {
Map<String, List<String>> params = createExtraParams();
Map<String, List<String>> params = createExtraParams(theCustomAcceptHeader);
if (clientInvocation instanceof HttpGetClientInvocation) {
if (myRequestFormatParamStyle == RequestFormatParamStyleEnum.SHORT) {
if (myRequestFormatParamStyle == RequestFormatParamStyleEnum.SHORT && isBlank(theCustomAcceptHeader)) {
if (theEncoding == EncodingEnum.XML) {
params.put(Constants.PARAM_FORMAT, Collections.singletonList("xml"));
} else if (theEncoding == EncodingEnum.JSON) {
@ -248,6 +251,11 @@ public abstract class BaseClient implements IRestfulClient {
httpRequest = clientInvocation.asHttpRequest(myUrlBase, params, encoding, thePrettyPrint);
if (isNotBlank(theCustomAcceptHeader)) {
httpRequest.removeHeaders(Constants.HEADER_ACCEPT);
httpRequest.addHeader(Constants.HEADER_ACCEPT, theCustomAcceptHeader);
}
if (theCacheControlDirective != null) {
StringBuilder b = new StringBuilder();
addToCacheControlHeader(b, Constants.CACHE_CONTROL_NO_CACHE, theCacheControlDirective.isNoCache());
@ -289,14 +297,10 @@ public abstract class BaseClient implements IRestfulClient {
if (response.getStatus() < 200 || response.getStatus() > 299) {
String body = null;
Reader reader = null;
try {
reader = response.createReader();
try (Reader reader = response.createReader()) {
body = IOUtils.toString(reader);
} catch (Exception e) {
ourLog.debug("Failed to read input stream", e);
} finally {
IOUtils.closeQuietly(reader);
}
String message = "HTTP " + response.getStatus() + " " + response.getStatusInfo();
@ -334,27 +338,22 @@ public abstract class BaseClient implements IRestfulClient {
if (binding instanceof IClientResponseHandlerHandlesBinary) {
IClientResponseHandlerHandlesBinary<T> handlesBinary = (IClientResponseHandlerHandlesBinary<T>) binding;
if (handlesBinary.isBinary()) {
InputStream reader = response.readEntity();
try {
try (InputStream reader = response.readEntity()) {
return handlesBinary.invokeClient(mimeType, reader, response.getStatus(), headers);
} finally {
IOUtils.closeQuietly(reader);
}
}
}
Reader reader = response.createReader();
try (InputStream inputStream = response.readEntity()) {
InputStream inputStreamToReturn = inputStream;
if (ourLog.isTraceEnabled() || myKeepResponses || theLogRequestAndResponse) {
String responseString = IOUtils.toString(reader);
keepResponseAndLogIt(theLogRequestAndResponse, response, responseString);
reader = new StringReader(responseString);
}
if (ourLog.isTraceEnabled() || myKeepResponses || theLogRequestAndResponse) {
String responseString = IOUtils.toString(inputStream, Charsets.UTF_8);
keepResponseAndLogIt(theLogRequestAndResponse, response, responseString);
inputStreamToReturn = new ByteArrayInputStream(responseString.getBytes(Charsets.UTF_8));
}
try {
return binding.invokeClient(mimeType, reader, response.getStatus(), headers);
} finally {
IOUtils.closeQuietly(reader);
return binding.invokeClient(mimeType, inputStreamToReturn, response.getStatus(), headers);
}
} catch (DataFormatException e) {
@ -463,7 +462,48 @@ public abstract class BaseClient implements IRestfulClient {
myInterceptors.remove(theInterceptor);
}
protected final class ResourceResponseHandler<T extends IBaseResource> implements IClientResponseHandler<T> {
protected final class ResourceOrBinaryResponseHandler extends ResourceResponseHandler<IBaseResource> {
@Override
public IBaseResource invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
/*
* For operation responses, if the response content type is a FHIR content-type
* (which is will probably almost always be) we just handle it normally. However,
* if we get back a successful (2xx) response from an operation, and the content
* type is something other than FHIR, we'll return it as a Binary wrapped in
* a Parameters resource.
*/
EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType);
if (respType != null || theResponseStatusCode < 200 || theResponseStatusCode >= 300) {
return super.invokeClient(theResponseMimeType, theResponseInputStream, theResponseStatusCode, theHeaders);
}
// Create a Binary resource to return
IBaseBinary responseBinary = BinaryUtil.newBinary(getFhirContext());
// Fetch the content type
String contentType = null;
List<String> contentTypeHeaders = theHeaders.get(Constants.HEADER_CONTENT_TYPE_LC);
if (contentTypeHeaders != null && contentTypeHeaders.size() > 0) {
contentType = contentTypeHeaders.get(0);
}
responseBinary.setContentType(contentType);
// Fetch the content itself
try {
responseBinary.setContent(IOUtils.toByteArray(theResponseInputStream));
} catch (IOException e) {
throw new InternalErrorException("IO failure parsing response", e);
}
return responseBinary;
}
}
protected class ResourceResponseHandler<T extends IBaseResource> implements IClientResponseHandler<T> {
private boolean myAllowHtmlResponse;
private IIdType myId;
@ -498,20 +538,20 @@ public abstract class BaseClient implements IRestfulClient {
}
@Override
public T invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
public T invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType);
if (respType == null) {
if (myAllowHtmlResponse && theResponseMimeType.toLowerCase().contains(Constants.CT_HTML) && myReturnType != null) {
return readHtmlResponse(theResponseReader);
return readHtmlResponse(theResponseInputStream);
}
throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseReader);
throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseInputStream);
}
IParser parser = respType.newParser(getFhirContext());
parser.setServerBaseUrl(getUrlBase());
if (myPreferResponseTypes != null) {
parser.setPreferTypes(myPreferResponseTypes);
}
T retVal = parser.parseResource(myReturnType, theResponseReader);
T retVal = parser.parseResource(myReturnType, theResponseInputStream);
MethodUtil.parseClientRequestResourceHeaders(myId, theHeaders, retVal);
@ -519,7 +559,7 @@ public abstract class BaseClient implements IRestfulClient {
}
@SuppressWarnings("unchecked")
private T readHtmlResponse(Reader theResponseReader) {
private T readHtmlResponse(InputStream theResponseInputStream) {
RuntimeResourceDefinition resDef = getFhirContext().getResourceDefinition(myReturnType);
IBaseResource instance = resDef.newInstance();
BaseRuntimeChildDefinition textChild = resDef.getChildByName("text");
@ -531,7 +571,7 @@ public abstract class BaseClient implements IRestfulClient {
BaseRuntimeElementDefinition<?> divElement = divChild.getChildByName("div");
IPrimitiveType<?> divInstance = (IPrimitiveType<?>) divElement.newInstance();
try {
divInstance.setValueAsString(IOUtils.toString(theResponseReader));
divInstance.setValueAsString(IOUtils.toString(theResponseInputStream, Charsets.UTF_8));
} catch (Exception e) {
throw new InvalidResponseException(400, "Failed to process HTML response from server: " + e.getMessage(), e);
}
@ -539,8 +579,9 @@ public abstract class BaseClient implements IRestfulClient {
return (T) instance;
}
public void setPreferResponseTypes(List<Class<? extends IBaseResource>> thePreferResponseTypes) {
public ResourceResponseHandler<T> setPreferResponseTypes(List<Class<? extends IBaseResource>> thePreferResponseTypes) {
myPreferResponseTypes = thePreferResponseTypes;
return this;
}
}

View File

@ -46,19 +46,19 @@ import ca.uhn.fhir.rest.server.exceptions.NotModifiedException;
import ca.uhn.fhir.util.ICallable;
import ca.uhn.fhir.util.ParametersUtil;
import ca.uhn.fhir.util.UrlUtil;
import com.google.common.base.Charsets;
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.util.*;
import java.util.Map.Entry;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.*;
/**
* @author James Agnew
@ -98,7 +98,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
private <T extends IBaseResource> T doReadOrVRead(final Class<T> theType, IIdType theId, boolean theVRead, ICallable<T> theNotModifiedHandler, String theIfVersionMatches, Boolean thePrettyPrint,
SummaryEnum theSummary, EncodingEnum theEncoding, Set<String> theSubsetElements) {
SummaryEnum theSummary, EncodingEnum theEncoding, Set<String> theSubsetElements, String theCustomAcceptHeaderValue) {
String resName = toResourceName(theType);
IIdType id = theId;
if (!id.hasBaseUrl()) {
@ -120,7 +120,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
}
if (isKeepResponses()) {
myLastRequest = invocation.asHttpRequest(getServerBase(), createExtraParams(), getEncoding(), isPrettyPrint());
myLastRequest = invocation.asHttpRequest(getServerBase(), createExtraParams(theCustomAcceptHeaderValue), getEncoding(), isPrettyPrint());
}
if (theIfVersionMatches != null) {
@ -131,10 +131,10 @@ public class GenericClient extends BaseClient implements IGenericClient {
ResourceResponseHandler<T> binding = new ResourceResponseHandler<>(theType, (Class<? extends IBaseResource>) null, id, allowHtmlResponse);
if (theNotModifiedHandler == null) {
return invokeClient(myContext, binding, invocation, theEncoding, thePrettyPrint, myLogRequestAndResponse, theSummary, theSubsetElements, null);
return invokeClient(myContext, binding, invocation, theEncoding, thePrettyPrint, myLogRequestAndResponse, theSummary, theSubsetElements, null, theCustomAcceptHeaderValue);
}
try {
return invokeClient(myContext, binding, invocation, theEncoding, thePrettyPrint, myLogRequestAndResponse, theSummary, theSubsetElements, null);
return invokeClient(myContext, binding, invocation, theEncoding, thePrettyPrint, myLogRequestAndResponse, theSummary, theSubsetElements, null, theCustomAcceptHeaderValue);
} catch (NotModifiedException e) {
return theNotModifiedHandler.call();
}
@ -228,7 +228,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
@Override
public <T extends IBaseResource> T read(final Class<T> theType, UriDt theUrl) {
IdDt id = theUrl instanceof IdDt ? ((IdDt) theUrl) : new IdDt(theUrl);
return doReadOrVRead(theType, id, false, null, null, false, null, null, null);
return doReadOrVRead(theType, id, false, null, null, false, null, null, null, null);
}
@Override
@ -269,7 +269,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
public MethodOutcome update(IdDt theIdDt, IBaseResource theResource) {
BaseHttpClientInvocation invocation = MethodUtil.createUpdateInvocation(theResource, null, theIdDt, myContext);
if (isKeepResponses()) {
myLastRequest = invocation.asHttpRequest(getServerBase(), createExtraParams(), getEncoding(), isPrettyPrint());
myLastRequest = invocation.asHttpRequest(getServerBase(), createExtraParams(null), getEncoding(), isPrettyPrint());
}
OutcomeResponseHandler binding = new OutcomeResponseHandler();
@ -293,7 +293,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
invocation = ValidateMethodBindingDstu2Plus.createValidateInvocation(myContext, theResource);
if (isKeepResponses()) {
myLastRequest = invocation.asHttpRequest(getServerBase(), createExtraParams(), getEncoding(), isPrettyPrint());
myLastRequest = invocation.asHttpRequest(getServerBase(), createExtraParams(null), getEncoding(), isPrettyPrint());
}
OutcomeResponseHandler binding = new OutcomeResponseHandler();
@ -306,7 +306,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
if (theId.hasVersionIdPart() == false) {
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);
return doReadOrVRead(theType, theId, true, null, null, false, null, null, null, null);
}
@Override
@ -315,49 +315,6 @@ public class GenericClient extends BaseClient implements IGenericClient {
return vread(theType, resId);
}
private static void addParam(Map<String, List<String>> params, String parameterName, String parameterValue) {
if (!params.containsKey(parameterName)) {
params.put(parameterName, new ArrayList<>());
}
params.get(parameterName).add(parameterValue);
}
private static void addPreferHeader(PreferReturnEnum thePrefer, BaseHttpClientInvocation theInvocation) {
if (thePrefer != null) {
theInvocation.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + '=' + thePrefer.getHeaderValue());
}
}
private static String validateAndEscapeConditionalUrl(String theSearchUrl) {
Validate.notBlank(theSearchUrl, "Conditional URL can not be blank/null");
StringBuilder b = new StringBuilder();
boolean haveHadQuestionMark = false;
for (int i = 0; i < theSearchUrl.length(); i++) {
char nextChar = theSearchUrl.charAt(i);
if (!haveHadQuestionMark) {
if (nextChar == '?') {
haveHadQuestionMark = true;
} else if (!Character.isLetter(nextChar)) {
throw new IllegalArgumentException("Conditional URL must be in the format \"[ResourceType]?[Params]\" and must not have a base URL - Found: " + theSearchUrl);
}
b.append(nextChar);
} else {
switch (nextChar) {
case '|':
case '?':
case '$':
case ':':
b.append(UrlUtil.escapeUrlParam(Character.toString(nextChar)));
break;
default:
b.append(nextChar);
break;
}
}
}
return b.toString();
}
private enum MetaOperation {
ADD,
DELETE,
@ -366,14 +323,25 @@ public class GenericClient extends BaseClient implements IGenericClient {
private abstract class BaseClientExecutable<T extends IClientExecutable<?, Y>, Y> implements IClientExecutable<T, Y> {
protected EncodingEnum myParamEncoding;
protected Boolean myPrettyPrint;
protected SummaryEnum mySummaryMode;
protected CacheControlDirective myCacheControlDirective;
EncodingEnum myParamEncoding;
Boolean myPrettyPrint;
SummaryEnum mySummaryMode;
CacheControlDirective myCacheControlDirective;
private String myCustomAcceptHeaderValue;
private List<Class<? extends IBaseResource>> myPreferResponseTypes;
private boolean myQueryLogRequestAndResponse;
private HashSet<String> mySubsetElements;
public String getCustomAcceptHeaderValue() {
return myCustomAcceptHeaderValue;
}
@Override
public T accept(String theHeaderValue) {
myCustomAcceptHeaderValue = theHeaderValue;
return (T) this;
}
@Deprecated // override deprecated method
@SuppressWarnings("unchecked")
@Override
@ -392,7 +360,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
@Override
public T elementsSubset(String... theElements) {
if (theElements != null && theElements.length > 0) {
mySubsetElements = new HashSet<String>(Arrays.asList(theElements));
mySubsetElements = new HashSet<>(Arrays.asList(theElements));
} else {
mySubsetElements = null;
}
@ -444,7 +412,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);
Z resp = invokeClient(myContext, theHandler, theInvocation, myParamEncoding, myPrettyPrint, myQueryLogRequestAndResponse || myLogRequestAndResponse, mySummaryMode, mySubsetElements, myCacheControlDirective, myCustomAcceptHeaderValue);
return resp;
}
@ -461,7 +429,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
public T preferResponseType(Class<? extends IBaseResource> theClass) {
myPreferResponseTypes = null;
if (theClass != null) {
myPreferResponseTypes = new ArrayList<Class<? extends IBaseResource>>();
myPreferResponseTypes = new ArrayList<>();
myPreferResponseTypes.add(theClass);
}
return (T) this;
@ -1021,14 +989,14 @@ public class GenericClient extends BaseClient implements IGenericClient {
@SuppressWarnings("unchecked")
@Override
public T invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
public T invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType);
if (respType == null) {
throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseReader);
throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseInputStream);
}
IParser parser = respType.newParser(myContext);
RuntimeResourceDefinition type = myContext.getResourceDefinition("Parameters");
IBaseResource retVal = parser.parseResource(type.getImplementingClass(), theResponseReader);
IBaseResource retVal = parser.parseResource(type.getImplementingClass(), theResponseInputStream);
BaseRuntimeChildDefinition paramChild = type.getChildByName("parameter");
BaseRuntimeElementCompositeDefinition<?> paramChildElem = (BaseRuntimeElementCompositeDefinition<?>) paramChild.getChildByName("parameter");
@ -1061,6 +1029,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
private Class myReturnResourceType;
private Class<? extends IBaseResource> myType;
private boolean myUseHttpGet;
private boolean myReturnMethodOutcome;
@SuppressWarnings("unchecked")
private void addParam(String theName, IBase theValue) {
@ -1170,11 +1139,19 @@ public class GenericClient extends BaseClient implements IGenericClient {
Object retVal = invoke(null, handler, invocation);
return retVal;
}
ResourceResponseHandler handler;
handler = new ResourceResponseHandler();
handler.setPreferResponseTypes(getPreferResponseTypes(myType));
IClientResponseHandler handler = new ResourceOrBinaryResponseHandler()
.setPreferResponseTypes(getPreferResponseTypes(myType));
if (myReturnMethodOutcome) {
handler = new MethodOutcomeResponseHandler(handler);
}
Object retVal = invoke(null, handler, invocation);
if (myReturnMethodOutcome) {
return retVal;
}
if (myContext.getResourceDefinition((IBaseResource) retVal).getName().equals("Parameters")) {
return retVal;
}
@ -1236,6 +1213,12 @@ public class GenericClient extends BaseClient implements IGenericClient {
return this;
}
@Override
public IOperationUntypedWithInput returnMethodOutcome() {
myReturnMethodOutcome = true;
return this;
}
@SuppressWarnings("unchecked")
@Override
public IOperationProcessMsgMode setMessageBundle(IBaseBundle theMsgBundle) {
@ -1331,10 +1314,30 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
private final class MethodOutcomeResponseHandler implements IClientResponseHandler<MethodOutcome> {
private final IClientResponseHandler<? extends IBaseResource> myWrap;
private MethodOutcomeResponseHandler(IClientResponseHandler<? extends IBaseResource> theWrap) {
myWrap = theWrap;
}
@Override
public MethodOutcome invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException, BaseServerResponseException {
IBaseResource response = myWrap.invokeClient(theResponseMimeType, theResponseInputStream, theResponseStatusCode, theHeaders);
MethodOutcome retVal = new MethodOutcome();
retVal.setResource(response);
retVal.setCreatedUsingStatusCode(theResponseStatusCode);
retVal.setResponseHeaders(theHeaders);
return retVal;
}
}
private final class OperationOutcomeResponseHandler implements IClientResponseHandler<IBaseOperationOutcome> {
@Override
public IBaseOperationOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders)
public IBaseOperationOutcome invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders)
throws BaseServerResponseException {
EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType);
if (respType == null) {
@ -1344,7 +1347,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
IBaseOperationOutcome retVal;
try {
// TODO: handle if something else than OO comes back
retVal = (IBaseOperationOutcome) parser.parseResource(theResponseReader);
retVal = (IBaseOperationOutcome) parser.parseResource(theResponseInputStream);
} catch (DataFormatException e) {
ourLog.warn("Failed to parse OperationOutcome response", e);
return null;
@ -1368,11 +1371,9 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
@Override
public MethodOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
MethodOutcome response = MethodUtil.process2xxResponse(myContext, theResponseStatusCode, theResponseMimeType, theResponseReader, theHeaders);
if (theResponseStatusCode == Constants.STATUS_HTTP_201_CREATED) {
response.setCreated(true);
}
public MethodOutcome invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
MethodOutcome response = MethodUtil.process2xxResponse(myContext, theResponseStatusCode, theResponseMimeType, theResponseInputStream, theHeaders);
response.setCreatedUsingStatusCode(theResponseStatusCode);
if (myPrefer == PreferReturnEnum.REPRESENTATION) {
if (response.getResource() == null) {
@ -1384,6 +1385,8 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
}
response.setResponseHeaders(theHeaders);
return response;
}
}
@ -1511,9 +1514,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());
return doReadOrVRead(myType.getImplementingClass(), myId, true, myNotModifiedHandler, myIfVersionMatches, myPrettyPrint, mySummaryMode, myParamEncoding, getSubsetElements(), getCustomAcceptHeaderValue());
}
return doReadOrVRead(myType.getImplementingClass(), myId, false, myNotModifiedHandler, myIfVersionMatches, myPrettyPrint, mySummaryMode, myParamEncoding, getSubsetElements());
return doReadOrVRead(myType.getImplementingClass(), myId, false, myNotModifiedHandler, myIfVersionMatches, myPrettyPrint, mySummaryMode, myParamEncoding, getSubsetElements(), getCustomAcceptHeaderValue());
}
@Override
@ -1636,11 +1639,11 @@ public class GenericClient extends BaseClient implements IGenericClient {
@SuppressWarnings("unchecked")
@Override
public List<IBaseResource> invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders)
public List<IBaseResource> invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders)
throws BaseServerResponseException {
Class<? extends IBaseResource> bundleType = myContext.getResourceDefinition("Bundle").getImplementingClass();
ResourceResponseHandler<IBaseResource> handler = new ResourceResponseHandler<IBaseResource>((Class<IBaseResource>) bundleType);
IBaseResource response = handler.invokeClient(theResponseMimeType, theResponseReader, theResponseStatusCode, theHeaders);
ResourceResponseHandler<IBaseResource> handler = new ResourceResponseHandler<>((Class<IBaseResource>) bundleType);
IBaseResource response = handler.invokeClient(theResponseMimeType, theResponseInputStream, theResponseStatusCode, theHeaders);
IVersionSpecificBundleFactory bundleFactory = myContext.newBundleFactory();
bundleFactory.initializeWithBundleResource(response);
return bundleFactory.toListOfResources();
@ -1937,9 +1940,9 @@ public class GenericClient extends BaseClient implements IGenericClient {
private final class StringResponseHandler implements IClientResponseHandler<String> {
@Override
public String invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders)
public String invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders)
throws IOException, BaseServerResponseException {
return IOUtils.toString(theResponseReader);
return IOUtils.toString(theResponseInputStream, Charsets.UTF_8);
}
}
@ -2251,4 +2254,47 @@ 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<>());
}
params.get(parameterName).add(parameterValue);
}
private static void addPreferHeader(PreferReturnEnum thePrefer, BaseHttpClientInvocation theInvocation) {
if (thePrefer != null) {
theInvocation.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + '=' + thePrefer.getHeaderValue());
}
}
private static String validateAndEscapeConditionalUrl(String theSearchUrl) {
Validate.notBlank(theSearchUrl, "Conditional URL can not be blank/null");
StringBuilder b = new StringBuilder();
boolean haveHadQuestionMark = false;
for (int i = 0; i < theSearchUrl.length(); i++) {
char nextChar = theSearchUrl.charAt(i);
if (!haveHadQuestionMark) {
if (nextChar == '?') {
haveHadQuestionMark = true;
} else if (!Character.isLetter(nextChar)) {
throw new IllegalArgumentException("Conditional URL must be in the format \"[ResourceType]?[Params]\" and must not have a base URL - Found: " + theSearchUrl);
}
b.append(nextChar);
} else {
switch (nextChar) {
case '|':
case '?':
case '$':
case ':':
b.append(UrlUtil.escapeUrlParam(Character.toString(nextChar)));
break;
default:
b.append(nextChar);
break;
}
}
}
return b.toString();
}
}

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.rest.client.method;
*/
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.lang.reflect.Method;
import java.util.*;
@ -71,11 +72,11 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
}
protected IParser createAppropriateParserForParsingResponse(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, List<Class<? extends IBaseResource>> thePreferTypes) {
protected IParser createAppropriateParserForParsingResponse(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, List<Class<? extends IBaseResource>> thePreferTypes) {
EncodingEnum encoding = EncodingEnum.forContentType(theResponseMimeType);
if (encoding == null) {
NonFhirResponseException ex = NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseReader);
populateException(ex, theResponseReader);
NonFhirResponseException ex = NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseInputStream);
populateException(ex, theResponseInputStream);
throw ex;
}
@ -139,7 +140,7 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
return mySupportsConditionalMultiple;
}
protected BaseServerResponseException processNon2xxResponseAndReturnExceptionToThrow(int theStatusCode, String theResponseMimeType, Reader theResponseReader) {
protected BaseServerResponseException processNon2xxResponseAndReturnExceptionToThrow(int theStatusCode, String theResponseMimeType, InputStream theResponseInputStream) {
BaseServerResponseException ex;
switch (theStatusCode) {
case Constants.STATUS_HTTP_400_BAD_REQUEST:
@ -158,9 +159,9 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
ex = new PreconditionFailedException("Server responded with HTTP 412");
break;
case Constants.STATUS_HTTP_422_UNPROCESSABLE_ENTITY:
IParser parser = createAppropriateParserForParsingResponse(theResponseMimeType, theResponseReader, theStatusCode, null);
IParser parser = createAppropriateParserForParsingResponse(theResponseMimeType, theResponseInputStream, theStatusCode, null);
// TODO: handle if something other than OO comes back
BaseOperationOutcome operationOutcome = (BaseOperationOutcome) parser.parseResource(theResponseReader);
BaseOperationOutcome operationOutcome = (BaseOperationOutcome) parser.parseResource(theResponseInputStream);
ex = new UnprocessableEntityException(myContext, operationOutcome);
break;
default:
@ -168,7 +169,7 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
break;
}
populateException(ex, theResponseReader);
populateException(ex, theResponseInputStream);
return ex;
}
@ -322,9 +323,9 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
return theReturnTypeFromMethod.equals(IBaseResource.class) || theReturnTypeFromMethod.equals(IResource.class) || theReturnTypeFromMethod.equals(IAnyResource.class);
}
private static void populateException(BaseServerResponseException theEx, Reader theResponseReader) {
private static void populateException(BaseServerResponseException theEx, InputStream theResponseInputStream) {
try {
String responseText = IOUtils.toString(theResponseReader);
String responseText = IOUtils.toString(theResponseInputStream);
theEx.setResponseBody(responseText);
} catch (IOException e) {
ourLog.debug("Failed to read response", e);

View File

@ -20,6 +20,7 @@ package ca.uhn.fhir.rest.client.method;
* #L%
*/
import java.io.InputStream;
import java.io.Reader;
import java.lang.reflect.Method;
import java.util.*;
@ -68,15 +69,15 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding<Metho
protected abstract String getMatchingOperation();
@Override
public MethodOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
public MethodOutcome invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
if (theResponseStatusCode >= 200 && theResponseStatusCode < 300) {
if (myReturnVoid) {
return null;
}
MethodOutcome retVal = MethodUtil.process2xxResponse(getContext(), theResponseStatusCode, theResponseMimeType, theResponseReader, theHeaders);
MethodOutcome retVal = MethodUtil.process2xxResponse(getContext(), theResponseStatusCode, theResponseMimeType, theResponseInputStream, theHeaders);
return retVal;
}
throw processNon2xxResponseAndReturnExceptionToThrow(theResponseStatusCode, theResponseMimeType, theResponseReader);
throw processNon2xxResponseAndReturnExceptionToThrow(theResponseStatusCode, theResponseMimeType, theResponseInputStream);
}
public boolean isReturnVoid() {

View File

@ -20,6 +20,8 @@ package ca.uhn.fhir.rest.client.method;
* #L%
*/
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
@ -123,21 +125,21 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
public abstract ReturnTypeEnum getReturnType();
@Override
public Object invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) {
public Object invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException {
if (Constants.STATUS_HTTP_204_NO_CONTENT == theResponseStatusCode) {
return toReturnType(null);
}
IParser parser = createAppropriateParserForParsingResponse(theResponseMimeType, theResponseReader, theResponseStatusCode, myPreferTypesList);
IParser parser = createAppropriateParserForParsingResponse(theResponseMimeType, theResponseInputStream, theResponseStatusCode, myPreferTypesList);
switch (getReturnType()) {
case BUNDLE: {
IBaseBundle bundle = null;
List<? extends IBaseResource> listOfResources = null;
IBaseBundle bundle;
List<? extends IBaseResource> listOfResources;
Class<? extends IBaseResource> type = getContext().getResourceDefinition("Bundle").getImplementingClass();
bundle = (IBaseBundle) parser.parseResource(type, theResponseReader);
bundle = (IBaseBundle) parser.parseResource(type, theResponseInputStream);
listOfResources = BundleUtil.toListOfResources(getContext(), bundle);
switch (getMethodReturnType()) {
@ -171,9 +173,9 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
case RESOURCE: {
IBaseResource resource;
if (myResourceType != null) {
resource = parser.parseResource(myResourceType, theResponseReader);
resource = parser.parseResource(myResourceType, theResponseInputStream);
} else {
resource = parser.parseResource(theResponseReader);
resource = parser.parseResource(theResponseInputStream);
}
MethodUtil.parseClientRequestResourceHeaders(null, theHeaders, resource);

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.rest.client.method;
*/
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.List;
import java.util.Map;
@ -29,6 +30,6 @@ import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
public interface IClientResponseHandler<T> {
T invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException, BaseServerResponseException;
T invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException, BaseServerResponseException;
}

View File

@ -49,15 +49,6 @@ import ca.uhn.fhir.util.*;
public class MethodUtil {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(MethodUtil.class);
private static final Set<String> ourServletRequestTypes = new HashSet<String>();
private static final Set<String> ourServletResponseTypes = new HashSet<String>();
static {
ourServletRequestTypes.add("javax.servlet.ServletRequest");
ourServletResponseTypes.add("javax.servlet.ServletResponse");
ourServletRequestTypes.add("javax.servlet.http.HttpServletRequest");
ourServletResponseTypes.add("javax.servlet.http.HttpServletResponse");
}
/** Non instantiable */
private MethodUtil() {
@ -497,8 +488,8 @@ public class MethodUtil {
}
public static MethodOutcome process2xxResponse(FhirContext theContext, int theResponseStatusCode,
String theResponseMimeType, Reader theResponseReader, Map<String, List<String>> theHeaders) {
List<String> locationHeaders = new ArrayList<String>();
String theResponseMimeType, InputStream theResponseReader, Map<String, List<String>> theHeaders) {
List<String> locationHeaders = new ArrayList<>();
List<String> lh = theHeaders.get(Constants.HEADER_LOCATION_LC);
if (lh != null) {
locationHeaders.addAll(lh);
@ -509,14 +500,14 @@ public class MethodUtil {
}
MethodOutcome retVal = new MethodOutcome();
if (locationHeaders != null && locationHeaders.size() > 0) {
if (locationHeaders.size() > 0) {
String locationHeader = locationHeaders.get(0);
BaseOutcomeReturningMethodBinding.parseContentLocation(theContext, retVal, locationHeader);
}
if (theResponseStatusCode != Constants.STATUS_HTTP_204_NO_CONTENT) {
EncodingEnum ct = EncodingEnum.forContentType(theResponseMimeType);
if (ct != null) {
PushbackReader reader = new PushbackReader(theResponseReader);
PushbackInputStream reader = new PushbackInputStream(theResponseReader);
try {
int firstByte = reader.read();

View File

@ -50,6 +50,7 @@ public class ServletRestfulResponse extends RestfulResponse<ServletRequestDetail
HttpServletResponse theHttpResponse = getRequestDetails().getServletResponse();
theHttpResponse.setStatus(stausCode);
theHttpResponse.setContentType(contentType);
theHttpResponse.setCharacterEncoding(null);
if (bin.getContent() == null || bin.getContent().length == 0) {
return theHttpResponse.getOutputStream();
}

View File

@ -370,46 +370,6 @@ public class GenericClientDstu3Test {
}
}
@Test
public void testAcceptHeaderWithEncodingSpecified() 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) throws Throwable {
return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"));
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
int idx = 0;
client.setEncoding(EncodingEnum.JSON);
client.search()
.forResource("Device")
.returnBundle(Bundle.class)
.execute();
assertEquals("http://example.com/fhir/Device?_format=json", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString()));
assertEquals("application/fhir+json;q=1.0, application/json+fhir;q=0.9", capt.getAllValues().get(idx).getFirstHeader(Constants.HEADER_ACCEPT).getValue());
idx++;
client.setEncoding(EncodingEnum.XML);
client.search()
.forResource("Device")
.returnBundle(Bundle.class)
.execute();
assertEquals("http://example.com/fhir/Device?_format=xml", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString()));
assertEquals("application/fhir+xml;q=1.0, application/xml+fhir;q=0.9", capt.getAllValues().get(idx).getFirstHeader(Constants.HEADER_ACCEPT).getValue());
idx++;
}
@Test
public void testBinaryCreateWithFhirContentType() throws Exception {
IParser p = ourCtx.newXmlParser();

View File

@ -52,6 +52,7 @@ import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
@ -113,7 +114,7 @@ public class GenericClientR4Test {
}
@Test
public void testAcceptHeaderWithEncodingSpecified() throws Exception {
public void testAcceptHeaderCustom() 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);
@ -130,26 +131,41 @@ public class GenericClientR4Test {
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
int idx = 0;
client.setEncoding(EncodingEnum.JSON);
client.search()
.forResource("Device")
.returnBundle(Bundle.class)
.execute();
assertEquals("http://example.com/fhir/Device?_format=json", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString()));
assertEquals("application/fhir+json;q=1.0, application/json+fhir;q=0.9", capt.getAllValues().get(idx).getFirstHeader(Constants.HEADER_ACCEPT).getValue());
idx++;
// Custom accept value
client.setEncoding(EncodingEnum.XML);
client.search()
.forResource("Device")
.returnBundle(Bundle.class)
.accept("application/json")
.execute();
assertEquals("http://example.com/fhir/Device?_format=xml", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString()));
assertEquals("application/fhir+xml;q=1.0, application/xml+fhir;q=0.9", capt.getAllValues().get(idx).getFirstHeader(Constants.HEADER_ACCEPT).getValue());
assertEquals("http://example.com/fhir/Device", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString()));
assertEquals("application/json", capt.getAllValues().get(idx).getFirstHeader(Constants.HEADER_ACCEPT).getValue());
idx++;
// Empty accept value
client.setEncoding(EncodingEnum.XML);
client.search()
.forResource("Device")
.returnBundle(Bundle.class)
.accept("")
.execute();
assertEquals("http://example.com/fhir/Device?_format=xml", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString()));
assertEquals(Constants.HEADER_ACCEPT_VALUE_XML_NON_LEGACY, capt.getAllValues().get(idx).getFirstHeader(Constants.HEADER_ACCEPT).getValue());
idx++;
// Null accept value
client.setEncoding(EncodingEnum.XML);
client.search()
.forResource("Device")
.returnBundle(Bundle.class)
.accept(null)
.execute();
assertEquals("http://example.com/fhir/Device?_format=xml", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString()));
assertEquals(Constants.HEADER_ACCEPT_VALUE_XML_NON_LEGACY, capt.getAllValues().get(idx).getFirstHeader(Constants.HEADER_ACCEPT).getValue());
idx++;
}
@Test
@ -217,7 +233,7 @@ public class GenericClientR4Test {
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
Binary bin = new Binary();
bin.setContent(new byte[] {0, 1, 2, 3, 4});
bin.setContent(new byte[]{0, 1, 2, 3, 4});
client.create().resource(bin).execute();
ourLog.info(Arrays.asList(capt.getAllValues().get(0).getAllHeaders()).toString());
@ -227,7 +243,7 @@ public class GenericClientR4Test {
assertEquals("application/fhir+xml;charset=utf-8", capt.getAllValues().get(0).getHeaders("Content-Type")[0].getValue().toLowerCase().replace(" ", ""));
assertEquals(Constants.HEADER_ACCEPT_VALUE_XML_NON_LEGACY, capt.getAllValues().get(0).getHeaders("Accept")[0].getValue());
assertArrayEquals(new byte[] {0, 1, 2, 3, 4}, ourCtx.newXmlParser().parseResource(Binary.class, extractBodyAsString(capt)).getContent());
assertArrayEquals(new byte[]{0, 1, 2, 3, 4}, ourCtx.newXmlParser().parseResource(Binary.class, extractBodyAsString(capt)).getContent());
}
@ -306,7 +322,7 @@ public class GenericClientR4Test {
when(myHttpResponse.getAllHeaders()).thenAnswer(new Answer<Header[]>() {
@Override
public Header[] answer(InvocationOnMock theInvocation) {
return new Header[] {new BasicHeader(Constants.HEADER_LOCATION, "http://foo.com/base/Patient/222/_history/3")};
return new Header[]{new BasicHeader(Constants.HEADER_LOCATION, "http://foo.com/base/Patient/222/_history/3")};
}
});
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
@ -355,7 +371,7 @@ public class GenericClientR4Test {
when(myHttpResponse.getAllHeaders()).thenAnswer(new Answer<Header[]>() {
@Override
public Header[] answer(InvocationOnMock theInvocation) {
return new Header[] {new BasicHeader(Constants.HEADER_LOCATION, "http://foo.com/base/Patient/222/_history/3")};
return new Header[]{new BasicHeader(Constants.HEADER_LOCATION, "http://foo.com/base/Patient/222/_history/3")};
}
});
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
@ -880,6 +896,85 @@ public class GenericClientR4Test {
assertEquals("true", ((IPrimitiveType<?>) output.getParameterFirstRep().getValue()).getValueAsString());
}
/**
* Invoke an operation that returns HTML
* as a response (a HAPI FHIR server could accomplish this by returning
* a Binary resource)
*/
@Test
public void testOperationReturningArbitraryBinaryContentTextual() throws Exception {
IParser p = ourCtx.newXmlParser();
Parameters inputParams = new Parameters();
inputParams.addParameter().setName("name").setValue(new BooleanType(true));
final String respString = "<html>VALUE</html>";
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", "text/html"));
when(myHttpResponse.getEntity().getContent()).thenAnswer(t -> new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8")));
when(myHttpResponse.getAllHeaders()).thenReturn(new Header[]{
new BasicHeader("content-type", "text/html")
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
MethodOutcome result = client
.operation()
.onServer()
.named("opname")
.withParameters(inputParams)
.returnMethodOutcome()
.execute();
assertEquals(Binary.class, result.getResource().getClass());
Binary binary = (Binary) result.getResource();
assertEquals(respString, new String(binary.getContent(), Charsets.UTF_8));
assertEquals("text/html", binary.getContentType());
assertEquals("http://example.com/fhir/$opname", capt.getAllValues().get(0).getURI().toASCIIString());
}
/**
* Invoke an operation that returns HTML
* as a response (a HAPI FHIR server could accomplish this by returning
* a Binary resource)
*/
@Test
public void testOperationReturningArbitraryBinaryContentNonTextual() throws Exception {
IParser p = ourCtx.newXmlParser();
Parameters inputParams = new Parameters();
inputParams.addParameter().setName("name").setValue(new BooleanType(true));
final byte[] respBytes = new byte[]{0,1,2,3,4,5,6,7,8,9,100};
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", "application/weird-numbers"));
when(myHttpResponse.getEntity().getContent()).thenAnswer(t -> new ByteArrayInputStream(respBytes));
when(myHttpResponse.getAllHeaders()).thenReturn(new Header[]{
new BasicHeader("content-Type", "application/weird-numbers")
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
MethodOutcome result = client
.operation()
.onServer()
.named("opname")
.withParameters(inputParams)
.returnMethodOutcome()
.execute();
assertEquals(Binary.class, result.getResource().getClass());
Binary binary = (Binary) result.getResource();
assertEquals("application/weird-numbers", binary.getContentType());
assertArrayEquals(respBytes, binary.getContent());
assertEquals("http://example.com/fhir/$opname", capt.getAllValues().get(0).getURI().toASCIIString());
}
@Test
public void testOperationType() throws Exception {
IParser p = ourCtx.newXmlParser();
@ -2036,7 +2131,7 @@ public class GenericClientR4Test {
when(myHttpResponse.getAllHeaders()).thenAnswer(new Answer<Header[]>() {
@Override
public Header[] answer(InvocationOnMock theInvocation) {
return new Header[] {new BasicHeader(Constants.HEADER_LOCATION, "http://foo.com/base/Patient/222/_history/3")};
return new Header[]{new BasicHeader(Constants.HEADER_LOCATION, "http://foo.com/base/Patient/222/_history/3")};
}
});
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
@ -2084,7 +2179,7 @@ public class GenericClientR4Test {
when(myHttpResponse.getAllHeaders()).thenAnswer(new Answer<Header[]>() {
@Override
public Header[] answer(InvocationOnMock theInvocation) {
return new Header[] {new BasicHeader(Constants.HEADER_LOCATION, "http://foo.com/base/Patient/222/_history/3")};
return new Header[]{new BasicHeader(Constants.HEADER_LOCATION, "http://foo.com/base/Patient/222/_history/3")};
}
});
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
@ -2137,7 +2232,7 @@ public class GenericClientR4Test {
Binary bin = new Binary();
bin.setContentType("application/foo");
bin.setContent(new byte[] {0, 1, 2, 3, 4});
bin.setContent(new byte[]{0, 1, 2, 3, 4});
client.create().resource(bin).execute();
ourLog.info(Arrays.asList(capt.getAllValues().get(0).getAllHeaders()).toString());
@ -2147,7 +2242,7 @@ public class GenericClientR4Test {
assertEquals("application/foo", capt.getAllValues().get(0).getHeaders("Content-Type")[0].getValue());
assertEquals(Constants.HEADER_ACCEPT_VALUE_XML_OR_JSON_NON_LEGACY, capt.getAllValues().get(0).getHeaders("Accept")[0].getValue());
assertArrayEquals(new byte[] {0, 1, 2, 3, 4}, extractBodyAsByteArray(capt));
assertArrayEquals(new byte[]{0, 1, 2, 3, 4}, extractBodyAsByteArray(capt));
}
@ -2215,7 +2310,7 @@ public class GenericClientR4Test {
when(myHttpResponse.getAllHeaders()).thenAnswer(new Answer<Header[]>() {
@Override
public Header[] answer(InvocationOnMock theInvocation) {
return new Header[] {};
return new Header[]{};
}
});
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));

View File

@ -2,10 +2,7 @@ package ca.uhn.fhir.rest.server.interceptor;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.api.BundleInclusionRule;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
@ -64,6 +61,23 @@ public class ResponseHighlightingInterceptorTest {
ourInterceptor.setShowResponseHeaders(new ResponseHighlighterInterceptor().isShowResponseHeaders());
}
/**
* Return a Binary response type - Client accepts text/html but is not a browser
*/
@Test
public void testBinaryOperationHtmlResponseFromProvider() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/html/$binaryOp");
httpGet.addHeader("Accept", "text/html");
CloseableHttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
status.close();
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals("text/html", status.getFirstHeader("content-type").getValue());
assertEquals("<html>DATA</html>", responseContent);
assertEquals("Attachment;", status.getFirstHeader("Content-Disposition").getValue());
}
@Test
public void testBinaryReadAcceptBrowser() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Binary/foo");
@ -848,6 +862,21 @@ public class ResponseHighlightingInterceptorTest {
return Collections.singletonList(p);
}
@Operation(name="binaryOp", idempotent = true)
public Binary binaryOp(@IdParam IdType theId) {
Binary retVal = new Binary();
retVal.setId(theId);
if (theId.getIdPart().equals("html")) {
retVal.setContent("<html>DATA</html>".getBytes(Charsets.UTF_8));
retVal.setContentType("text/html");
}else {
retVal.setContent(new byte[]{1, 2, 3, 4});
retVal.setContentType(theId.getIdPart());
}
return retVal;
}
Map<String, Patient> getIdToPatient() {
Map<String, Patient> idToPatient = new HashMap<>();
{

View File

@ -19,7 +19,7 @@
<action type="fix">
The JPA server $expunge operation could sometimes fail to expunge if
another resource linked to a resource that was being
expunged. This has been corrected. In addition, the $expunge operation
expunged. This has been corrected. In addition, the $expunge operation
has been refactored to use smaller chunks of work
within a single DB transaction. This improves performance and reduces contention when
performing large expunge workloads.
@ -41,11 +41,22 @@
</action>
<action type="fix">
The ResponseHighlighterInterceptor now declines to handle Binary responses
provided as a response from extended operations. In other words if the
provided as a response from extended operations. In other words if the
operation $foo returns a Binary resource, the ResponseHighliterInterceptor will
not provide syntax highlighting on the response. This was previously the case for
the /Binary endpoint, but not for other binary responses.
</action>
<action type="add">
FHIR Parser now has an additional overload of the
<![CDATA[<code>parseResource</code>]]> method that accepts
an InputStream instead of a Reader as the source.
</action>
<action type="add">
FHIR Fluent/Generic Client now has a new return option called
<![CDATA[<code>returnMethodOutcome</code>]]> which can be
used to return a raw response. This is handy for invoking operations
that might return arbitrary binary content.
</action>
</release>
<release version="3.6.0" date="2018-11-12" description="Food">
<action type="add">
@ -77,7 +88,7 @@
<action type="add">
The module which deletes stale searches has been modified so that it deletes very large
searches (searches with 10000+ results in the query cache) in smaller batches, in order
to avoid having very long running delete operations occupying database connections for a
to avoid having very long running delete operations occupying database connections for a
long time or timing out.
</action>
<action type="fix">
@ -89,9 +100,9 @@
A new operation has been added to the JPA server called
<![CDATA[<code>$trigger-subscription</code>]]>. This can
be used to cause a transaction to redeliver a resource that previously triggered.
See
See
<![CDATA[<a href="https://smilecdr.com/docs/current/fhir_repository/subscription.html#manually-triggering-subscriptions">this link</a>]]>
for a description of how this feature works. Note that you must add the
for a description of how this feature works. Note that you must add the
SubscriptionRetriggeringProvider as shown in the sample project
<![CDATA[<a href="https://github.com/hapifhir/hapi-fhir-jpaserver-starter/blob/master/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java">here</a>.]]>
</action>