Don't log broken pipe exception when client disconnects (#4399)

* Avoid error on broken pipe

* Cleanup

* Add changelog

* Test fix

* Remove fixme

* Fix tests

* Fix build

* ddress warning

* Troubleshoot code

* More test logging

* Add debug logging

* Add logging

* Roll back changes

* Test fix

* Address review comments
This commit is contained in:
James Agnew 2023-01-06 11:32:29 -05:00 committed by GitHub
parent d70d813249
commit 31e4f039ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 872 additions and 594 deletions

View File

@ -0,0 +1,5 @@
---
type: fix
title: "When running RestfulServer under heavy load or a slow network, the server sometimes
logged an EOFException or an IOException in the system logs despite the response completing
successfully. This has been corrected."

View File

@ -36,6 +36,7 @@ import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jaxrs.server.util.JaxRsRequest;
import ca.uhn.fhir.jaxrs.server.util.JaxRsRequest.Builder;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.api.Constants;
@ -47,6 +48,7 @@ import ca.uhn.fhir.rest.server.HardcodedServerAddressStrategy;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.ResourceBinding;
import ca.uhn.fhir.rest.server.RestfulServerConfiguration;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
import ca.uhn.fhir.rest.server.provider.ServerCapabilityStatementProvider;
import ca.uhn.fhir.util.ReflectionUtil;
@ -214,7 +216,8 @@ public abstract class AbstractJaxRsConformanceProvider extends AbstractJaxRsProv
setUpPostConstruct();
Builder request = getRequest(RequestTypeEnum.OPTIONS, RestOperationTypeEnum.METADATA);
IRestfulResponse response = request.build().getResponse();
JaxRsRequest requestDetails = request.build();
IRestfulResponse response = requestDetails.getResponse();
response.addHeader(Constants.HEADER_CORS_ALLOW_ORIGIN, "*");
IBaseResource conformance;
@ -239,11 +242,9 @@ public abstract class AbstractJaxRsConformanceProvider extends AbstractJaxRsProv
throw new ConfigurationException(Msg.code(592) + "Unsupported Fhir version: " + fhirContextVersion);
}
if (conformance != null) {
Set<SummaryEnum> summaryMode = Collections.emptySet();
return (Response) response.streamResponseAsResource(conformance, false, summaryMode, Constants.STATUS_HTTP_200_OK, null, true, false);
}
return (Response) response.returnResponse(null, Constants.STATUS_HTTP_500_INTERNAL_ERROR, true, null, getResourceType().getSimpleName());
return (Response) RestfulServerUtils.streamResponseAsResource(this, conformance, summaryMode, Constants.STATUS_HTTP_200_OK, false, true, requestDetails, null, null);
}
/**

View File

@ -1,20 +1,17 @@
package ca.uhn.fhir.jaxrs.server.util;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.server.BaseParseAction;
import ca.uhn.fhir.rest.server.BaseRestfulResponse;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.util.IoUtil;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseBinary;
import org.apache.commons.lang3.Validate;
import javax.ws.rs.core.MediaType;
import javax.annotation.Nonnull;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import java.io.IOException;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.Writer;
import java.util.List;
@ -49,6 +46,12 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
*/
public class JaxRsResponse extends BaseRestfulResponse<JaxRsRequest> {
private StringWriter myWriter;
private int myStatusCode;
private String myContentType;
private String myCharset;
private ByteArrayOutputStream myOutputStream;
/**
* The constructor
*
@ -62,49 +65,54 @@ public class JaxRsResponse extends BaseRestfulResponse<JaxRsRequest> {
* The response writer is a simple String Writer. All output is configured
* by the server.
*/
@Nonnull
@Override
public Writer getResponseWriter(int theStatusCode, String theStatusMessage, String theContentType, String theCharset, boolean theRespondGzip) {
return new StringWriter();
public Writer getResponseWriter(int theStatusCode, String theContentType, String theCharset, boolean theRespondGzip) {
Validate.isTrue(myWriter == null, "getResponseWriter() called multiple times");
Validate.isTrue(myOutputStream == null, "getResponseWriter() called after getResponseOutputStream()");
myWriter = new StringWriter();
myStatusCode = theStatusCode;
myContentType = theContentType;
myCharset = theCharset;
return myWriter;
}
@Nonnull
@Override
public OutputStream getResponseOutputStream(int theStatusCode, String theContentType, Integer theContentLength) {
Validate.isTrue(myWriter == null, "getResponseOutputStream() called multiple times");
Validate.isTrue(myOutputStream == null, "getResponseOutputStream() called after getResponseWriter()");
myOutputStream = new ByteArrayOutputStream();
myStatusCode = theStatusCode;
myContentType = theContentType;
return myOutputStream;
}
@Override
public Response sendWriterResponse(int theStatus, String theContentType, String theCharset, Writer theWriter) {
ResponseBuilder builder = buildResponse(theStatus);
if (isNotBlank(theContentType)) {
String charContentType = theContentType + "; charset=" + StringUtils.defaultIfBlank(theCharset, Constants.CHARSET_NAME_UTF8);
public Response commitResponse(@Nonnull Closeable theWriterOrOutputStream) {
IoUtil.closeQuietly(theWriterOrOutputStream);
ResponseBuilder builder = buildResponse(myStatusCode);
if (isNotBlank(myContentType)) {
if (myWriter != null) {
String charContentType = myContentType + "; charset=" + StringUtils.defaultIfBlank(myCharset, Constants.CHARSET_NAME_UTF8);
builder.header(Constants.HEADER_CONTENT_TYPE, charContentType);
builder.entity(myWriter.toString());
} else {
byte[] byteArray = myOutputStream.toByteArray();
if (byteArray.length > 0) {
builder.header(Constants.HEADER_CONTENT_TYPE, myContentType);
builder.entity(byteArray);
}
builder.entity(theWriter.toString());
}
}
Response retVal = builder.build();
return retVal;
}
@Override
public Object sendAttachmentResponse(IBaseBinary bin, int statusCode, String contentType) {
ResponseBuilder response = buildResponse(statusCode);
if (bin.getContent() != null && bin.getContent().length > 0) {
response.header(Constants.HEADER_CONTENT_TYPE, contentType).entity(bin.getContent());
}
return response.build();
}
@Override
public Response returnResponse(BaseParseAction<?> outcome, int operationStatus, boolean allowPrefer,
MethodOutcome response, String resourceName) throws IOException {
StringWriter writer = new StringWriter();
if (outcome != null) {
FhirContext fhirContext = getRequestDetails().getServer().getFhirContext();
IParser parser = RestfulServerUtils.getNewParser(fhirContext, fhirContext.getVersion().getVersion(), getRequestDetails());
outcome.execute(parser, writer);
}
return sendWriterResponse(operationStatus, getParserType(), null, writer);
}
protected String getParserType() {
EncodingEnum encodingEnum = RestfulServerUtils.determineResponseEncodingWithDefault(getRequestDetails()).getEncoding();
return encodingEnum == EncodingEnum.JSON ? MediaType.APPLICATION_JSON : MediaType.APPLICATION_XML;
}
private ResponseBuilder buildResponse(int statusCode) {
ResponseBuilder response = Response.status(statusCode);
for (Entry<String, List<String>> header : getHeaders().entrySet()) {

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.jaxrs.server.util;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -61,7 +62,7 @@ public class JaxRsResponseDstu3Test {
Response result = (Response) RestfulServerUtils.streamResponseAsResource(request.getServer(), binary, theSummaryMode, 200, theAddContentLocationHeader, respondGzip, this.request);
assertEquals(200, result.getStatus());
assertEquals(contentType, result.getHeaderString(Constants.HEADER_CONTENT_TYPE));
assertEquals(content, result.getEntity());
assertArrayEquals(content, (byte[])result.getEntity());
}
@Test

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.jaxrs.server.util;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -48,7 +49,7 @@ public class JaxRsResponseTest {
Response result = (Response) RestfulServerUtils.streamResponseAsResource(request.getServer(), binary, theSummaryMode, 200, theAddContentLocationHeader, respondGzip, this.request);
assertEquals(200, result.getStatus());
assertEquals(contentType, result.getHeaderString(Constants.HEADER_CONTENT_TYPE));
assertEquals(content, result.getEntity());
assertArrayEquals(content, (byte[])result.getEntity());
}
@Test

View File

@ -4,8 +4,6 @@ import ca.uhn.fhir.cql.BaseCqlR4Test;
import ca.uhn.fhir.cql.r4.provider.MeasureOperationsProvider;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.MeasureReport;
import org.junit.Ignore;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@ -41,12 +39,7 @@ public class CqlMeasureEvaluationR4ImmunizationTest extends BaseCqlR4Test {
return this.myMeasureOperationsProvider.evaluateMeasure(new IdType("Measure", theMeasureId), evaluationDate, evaluationDate, null, "subject", thePatientRef, null, thePractitionerRef, null, null, null, null, myRequestDetails);
}
/**
* Disabled 2023-01-04 - Ticket to re-enable:
* https://github.com/hapifhir/hapi-fhir/issues/4401
*/
@Test
@Disabled
public void test_Immunization_Ontario_Schedule() throws IOException {
//given
loadBundle(MY_FHIR_COMMON);

View File

@ -63,7 +63,8 @@ public class ClientThreadedCapabilitiesTest {
@BeforeEach
public void beforeEach() throws Exception {
ourServer.getFhirClient().registerInterceptor(myCountingMetaClientInterceptor);
myClient = ourServer.getFhirClient();
myClient.registerInterceptor(myCountingMetaClientInterceptor);
}
@ -92,7 +93,7 @@ public class ClientThreadedCapabilitiesTest {
private Object searchPatient(String last) {
return ourServer.getFhirClient().search()
return myClient.search()
.forResource("Patient")
.returnBundle(Bundle.class)
.where(Patient.FAMILY.matches().value(last))

View File

@ -53,9 +53,11 @@ import java.util.List;
import static java.util.Arrays.asList;
import static org.apache.commons.lang3.time.DateUtils.MILLIS_PER_SECOND;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -244,14 +246,17 @@ public class ResourceProviderInterceptorR4Test extends BaseResourceProviderR4Tes
p.setActive(true);
IIdType pid = myClient.create().resource(p).execute().getId().toUnqualifiedVersionless();
await()
.until(()->{
Bundle observations = myClient
.search()
.forResource("Observation")
.where(Observation.SUBJECT.hasId(pid))
.returnBundle(Bundle.class)
.execute();
assertEquals(1, observations.getEntry().size());
ourLog.info(myFhirContext.newXmlParser().setPrettyPrint(true).encodeResourceToString(observations));
return observations.getEntry().size();
},
equalTo(1));
} finally {
myServer.getRestfulServer().unregisterInterceptor(interceptor);

View File

@ -20,42 +20,96 @@ package ca.uhn.fhir.rest.api.server;
* #L%
*/
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.SummaryEnum;
import org.hl7.fhir.instance.model.api.IBaseBinary;
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 javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
public interface IRestfulResponse {
Object streamResponseAsResource(IBaseResource theActualResourceToReturn, boolean thePrettyPrint, Set<SummaryEnum> theSummaryMode, int theStatusCode, String theStatusMessage, boolean theRespondGzip, boolean theAddContentLocation) throws IOException;
/**
* This is only used for DSTU1 getTags operations, so it can be removed at some point when we
* drop DSTU1
* Implementations of this interface represent a response back to the client from the server. It is
* conceptually similar to {@link javax.servlet.http.HttpServletResponse} but intended to be agnostic
* of the server framework being used.
* <p>
* This class is a bit of an awkward abstraction given the two styles of servers it supports.
* Servlets work by writing to a servlet response that is provided as a parameter by the container. JAX-RS
* works by returning an object via a method return back to the containing framework. However using it correctly should
* make for compatible code across both approaches.
* </p>
*/
Object returnResponse(BaseParseAction<?> outcome, int operationStatus, boolean allowPrefer, MethodOutcome response, String resourceName) throws IOException;
public interface IRestfulResponse {
Writer getResponseWriter(int theStatusCode, String theStatusMessage, String theContentType, String theCharset, boolean theRespondGzip) throws IOException;
/**
* Initiate a new textual response. The Writer returned by this method must be finalized by
* calling {@link #commitResponse(Closeable)} later.
* <p>
* Note that the caller should not close the returned object, but should instead just
* return it to {@link #commitResponse(Closeable)} upon successful completion. This is
* different from normal Java practice where you would request it in a <code>try with resource</code>
* block, since in Servlets you are not actually required to close the writer/stream, and
* doing so automatically may prevent you from correctly handling exceptions.
* </p>
*
* @param theStatusCode The HTTP status code.
* @param theContentType The HTTP response content type.
* @param theCharset The HTTP response charset.
* @param theRespondGzip Should the response be GZip encoded?
* @return Returns a {@link Writer} that can accept the response body.
*/
@Nonnull
Writer getResponseWriter(int theStatusCode, String theContentType, String theCharset, boolean theRespondGzip) throws IOException;
Object sendWriterResponse(int status, String contentType, String charset, Writer writer) throws IOException;
/**
* Initiate a new binary response. The OutputStream returned by this method must be finalized by
* calling {@link #commitResponse(Closeable)} later. This method should only be used for non-textual
* responses, for those use {@link #getResponseWriter(int, String, String, boolean)}.
* <p>
* Note that the caller should not close the returned object, but should instead just
* return it to {@link #commitResponse(Closeable)} upon successful completion. This is
* different from normal Java practice where you would request it in a <code>try with resource</code>
* block, since in Servlets you are not actually required to close the writer/stream, and
* doing so automatically may prevent you from correctly handling exceptions.
* </p>
*
* @param theStatusCode The HTTP status code.
* @param theContentType The HTTP response content type.
* @param theContentLength If known, the number of bytes that will be written. {@literal null} otherwise.
* @return Returns an {@link OutputStream} that can accept the response body.
*/
@Nonnull
OutputStream getResponseOutputStream(int theStatusCode, String theContentType, @Nullable Integer theContentLength) throws IOException;
/**
* Finalizes the response streaming using the writer that was returned by calling either
* {@link #getResponseWriter(int, String, String, boolean)} or
* {@link #getResponseOutputStream(int, String, Integer)}. This method should only be
* called if the response writing/streaming actually completed successfully. If an error
* occurred you do not need to commit the response.
*
* @param theWriterOrOutputStream The {@link Writer} or {@link OutputStream} that was returned by this object, or a Writer/OutputStream
* which decorates the one returned by this object.
* @return If the server style requires a returned response object (i.e. JAX-RS Server), this method
* returns that object. If the server style does not require one (i.e. {@link ca.uhn.fhir.rest.server.RestfulServer}),
* this method returns {@literal null}.
*/
Object commitResponse(@Nonnull Closeable theWriterOrOutputStream) throws IOException;
/**
* Adds a response header. This method must be called prior to calling
* {@link #getResponseWriter(int, String, String, boolean)} or {@link #getResponseOutputStream(int, String, Integer)}.
*
* @param headerKey The header name
* @param headerValue The header value
*/
void addHeader(String headerKey, String headerValue);
Object sendAttachmentResponse(IBaseBinary bin, int stausCode, String contentType) throws IOException;
void setOperationResourceLastUpdated(IPrimitiveType<Date> theOperationResourceLastUpdated);
/**
* Returns the headers added to this response
*/
Map<String, List<String>> getHeaders();
void setOperationResourceId(IIdType theOperationResourceId);
}

View File

@ -20,30 +20,20 @@ package ca.uhn.fhir.rest.server;
* #L%
*/
import ca.uhn.fhir.rest.api.SummaryEnum;
import ca.uhn.fhir.rest.api.server.IRestfulResponse;
import ca.uhn.fhir.rest.api.server.RequestDetails;
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 java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public abstract class BaseRestfulResponse<T extends RequestDetails> implements IRestfulResponse {
private IIdType myOperationResourceId;
private IPrimitiveType<Date> myOperationResourceLastUpdated;
private final Map<String, List<String>> myHeaders = new HashMap<>();
private T theRequestDetails;
private T myRequestDetails;
public BaseRestfulResponse(T requestDetails) {
this.theRequestDetails = requestDetails;
public BaseRestfulResponse(T theRequestDetails) {
this.myRequestDetails = theRequestDetails;
}
@Override
@ -53,6 +43,7 @@ public abstract class BaseRestfulResponse<T extends RequestDetails> implements I
/**
* Get the http headers
*
* @return the headers
*/
@Override
@ -62,36 +53,20 @@ public abstract class BaseRestfulResponse<T extends RequestDetails> implements I
/**
* Get the requestDetails
*
* @return the requestDetails
*/
public T getRequestDetails() {
return theRequestDetails;
}
@Override
public void setOperationResourceId(IIdType theOperationResourceId) {
myOperationResourceId = theOperationResourceId;
}
@Override
public void setOperationResourceLastUpdated(IPrimitiveType<Date> theOperationResourceLastUpdated) {
myOperationResourceLastUpdated = theOperationResourceLastUpdated;
return myRequestDetails;
}
/**
* Set the requestDetails
*
* @param requestDetails the requestDetails to set
*/
public void setRequestDetails(T requestDetails) {
this.theRequestDetails = requestDetails;
}
@Override
public final Object streamResponseAsResource(IBaseResource theResource, boolean thePrettyPrint, Set<SummaryEnum> theSummaryMode,
int theStatusCode, String theStatusMessage, boolean theRespondGzip, boolean theAddContentLocation)
throws IOException {
return RestfulServerUtils.streamResponseAsResource(theRequestDetails.getServer(), theResource, theSummaryMode, theStatusCode, theStatusMessage, theAddContentLocation, theRespondGzip, getRequestDetails(), myOperationResourceId, myOperationResourceLastUpdated);
this.myRequestDetails = requestDetails;
}
}

View File

@ -60,6 +60,7 @@ import ca.uhn.fhir.rest.server.method.MethodMatchEnum;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.rest.server.tenant.ITenantIdentificationStrategy;
import ca.uhn.fhir.util.CoverageIgnore;
import ca.uhn.fhir.util.IoUtil;
import ca.uhn.fhir.util.OperationOutcomeUtil;
import ca.uhn.fhir.util.ReflectionUtil;
import ca.uhn.fhir.util.UrlPathTokenizer;
@ -84,6 +85,7 @@ import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Closeable;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
@ -1166,7 +1168,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
* This is basically the end of processing for a successful request, since the
* method binding replies to the client and closes the response.
*/
try (Closeable outputStreamOrWriter = (Closeable) resourceMethod.invokeServer(this, requestDetails)) {
resourceMethod.invokeServer(this, requestDetails);
// Invoke interceptors
HookParams hookParams = new HookParams();
@ -1174,9 +1176,6 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
hookParams.add(ServletRequestDetails.class, requestDetails);
myInterceptorService.callHooks(Pointcut.SERVER_PROCESSING_COMPLETED_NORMALLY, hookParams);
ourLog.trace("Done writing to stream: {}", outputStreamOrWriter);
}
} catch (NotModifiedException | AuthenticationException e) {
HookParams handleExceptionParams = new HookParams();

View File

@ -20,9 +20,9 @@ package ca.uhn.fhir.rest.server;
* #L%
*/
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.model.api.IResource;
@ -50,6 +50,7 @@ import ca.uhn.fhir.rest.server.method.SummaryEnumParameter;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.BinaryUtil;
import ca.uhn.fhir.util.DateUtils;
import ca.uhn.fhir.util.IoUtil;
import ca.uhn.fhir.util.UrlUtil;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
@ -65,6 +66,7 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.util.Arrays;
import java.util.Collections;
@ -155,6 +157,7 @@ public class RestfulServerUtils {
}
}
@SuppressWarnings("EnumSwitchStatementWhichMissesCases" )
public static void configureResponseParser(RequestDetails theRequestDetails, IParser parser) {
// Pretty print
boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theRequestDetails.getServer(), theRequestDetails);
@ -885,12 +888,12 @@ public class RestfulServerUtils {
return prettyPrint;
}
public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource, Set<SummaryEnum> theSummaryMode, int stausCode, boolean theAddContentLocationHeader,
public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource, Set<SummaryEnum> theSummaryMode, int theStatusCode, boolean theAddContentLocationHeader,
boolean respondGzip, RequestDetails theRequestDetails) throws IOException {
return streamResponseAsResource(theServer, theResource, theSummaryMode, stausCode, null, theAddContentLocationHeader, respondGzip, theRequestDetails, null, null);
return streamResponseAsResource(theServer, theResource, theSummaryMode, theStatusCode, theAddContentLocationHeader, respondGzip, theRequestDetails, null, null);
}
public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource, Set<SummaryEnum> theSummaryMode, int theStatusCode, String theStatusMessage,
public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource, Set<SummaryEnum> theSummaryMode, int theStatusCode,
boolean theAddContentLocationHeader, boolean respondGzip, RequestDetails theRequestDetails, IIdType theOperationResourceId, IPrimitiveType<Date> theOperationResourceLastUpdated)
throws IOException {
IRestfulResponse response = theRequestDetails.getResponse();
@ -956,7 +959,16 @@ public class RestfulServerUtils {
contentType = getBinaryContentTypeOrDefault(bin);
response.addHeader(Constants.HEADER_CONTENT_DISPOSITION, "Attachment;" );
return response.sendAttachmentResponse(bin, theStatusCode, contentType);
Integer contentLength = null;
if (bin.hasData()) {
contentLength = bin.getContent().length;
}
OutputStream outputStream = response.getResponseOutputStream(theStatusCode, contentType, contentLength);
if (bin.hasData()) {
outputStream.write(bin.getContent());
}
return response.commitResponse(outputStream);
}
}
@ -1004,7 +1016,7 @@ public class RestfulServerUtils {
}
String charset = Constants.CHARSET_NAME_UTF8;
Writer writer = response.getResponseWriter(theStatusCode, theStatusMessage, contentType, charset, respondGzip);
Writer writer = response.getResponseWriter(theStatusCode, contentType, charset, respondGzip);
// Interceptor call: SERVER_OUTGOING_WRITER_CREATED
if (theServer.getInterceptorService() != null && theServer.getInterceptorService().hasHooks(Pointcut.SERVER_OUTGOING_WRITER_CREATED)) {
@ -1036,7 +1048,7 @@ public class RestfulServerUtils {
parser.encodeResourceToWriter(theResource, writer);
}
return response.sendWriterResponse(theStatusCode, contentType, charset, writer);
return response.commitResponse(writer);
}
private static String getBinaryContentTypeOrDefault(IBaseBinary theBinary) {
@ -1064,7 +1076,8 @@ public class RestfulServerUtils {
String contentType = theBinary.getContentType();
if (theResponseEncoding == null) {
return true;
} if (isBlank(contentType)) {
}
if (isBlank(contentType)) {
return Constants.CT_OCTET_STREAM.equals(theResponseEncoding.getContentType());
} else if (contentType.equalsIgnoreCase(theResponseEncoding.getContentType())) {
return true;

View File

@ -30,6 +30,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
@ -39,6 +40,7 @@ import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
import ca.uhn.fhir.rest.server.servlet.ServletRestfulResponse;
@ -61,12 +63,12 @@ public class ExceptionHandlingInterceptor {
public static final String PROCESSING = Constants.OO_INFOSTATUS_PROCESSING;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExceptionHandlingInterceptor.class);
public static final Set<SummaryEnum> SUMMARY_MODE = Collections.singleton(SummaryEnum.FALSE);
private Class<?>[] myReturnStackTracesForExceptionTypes;
@Hook(Pointcut.SERVER_HANDLE_EXCEPTION)
public boolean handleException(RequestDetails theRequestDetails, BaseServerResponseException theException, HttpServletRequest theRequest, HttpServletResponse theResponse) throws ServletException, IOException {
Closeable writer = (Closeable) handleException(theRequestDetails, theException);
writer.close();
handleException(theRequestDetails, theException);
return false;
}
@ -110,8 +112,7 @@ public class ExceptionHandlingInterceptor {
ourLog.error("HAPI-FHIR was unable to reset the output stream during exception handling. The root causes follows:", t);
}
return response.streamResponseAsResource(oo, true, Collections.singleton(SummaryEnum.FALSE), statusCode, statusMessage, false, false);
return RestfulServerUtils.streamResponseAsResource(theRequestDetails.getServer(), oo, SUMMARY_MODE, statusCode, false, false, theRequestDetails, null, null);
}
/**

View File

@ -36,10 +36,12 @@ import org.apache.commons.lang3.StringUtils;
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 java.io.IOException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Date;
import java.util.Set;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
@ -219,12 +221,14 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding {
IRestfulResponse restfulResponse = theRequest.getResponse();
IIdType responseId = null;
IPrimitiveType<Date> operationResourceLastUpdated = null;
if (theMethodOutcome != null) {
if (theMethodOutcome.getResource() != null) {
restfulResponse.setOperationResourceLastUpdated(RestfulServerUtils.extractLastUpdatedFromResource(theMethodOutcome.getResource()));
operationResourceLastUpdated = RestfulServerUtils.extractLastUpdatedFromResource(theMethodOutcome.getResource());
}
IIdType responseId = theMethodOutcome.getId();
responseId = theMethodOutcome.getId();
if (responseId != null && responseId.getResourceType() == null && responseId.hasIdPart()) {
responseId = responseId.withResourceType(getResourceName());
}
@ -232,14 +236,11 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding {
if (responseId != null) {
String serverBase = theRequest.getFhirServerBase();
responseId = RestfulServerUtils.fullyQualifyResourceIdOrReturnNull(theServer, theMethodOutcome.getResource(), serverBase, responseId);
restfulResponse.setOperationResourceId(responseId);
}
}
boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theServer, theRequest);
Set<SummaryEnum> summaryMode = Collections.emptySet();
return restfulResponse.streamResponseAsResource(responseDetails.getResponseResource(), prettyPrint, summaryMode, responseDetails.getResponseCode(), null, theRequest.isRespondGzip(), true);
return RestfulServerUtils.streamResponseAsResource(theServer, responseDetails.getResponseResource(), summaryMode, responseDetails.getResponseCode(), true, theRequest.isRespondGzip(), theRequest, responseId, operationResourceLastUpdated);
}
}

View File

@ -445,9 +445,8 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
if (!callOutgoingResponseHook(theRequest, responseDetails)) {
return null;
}
boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theServer, theRequest);
return theRequest.getResponse().streamResponseAsResource(responseDetails.getResponseResource(), prettyPrint, summaryMode, responseDetails.getResponseCode(), null, theRequest.isRespondGzip(), isAddContentLocationHeader());
return RestfulServerUtils.streamResponseAsResource(theServer, responseDetails.getResponseResource(), summaryMode, responseDetails.getResponseCode(), isAddContentLocationHeader(), theRequest.isRespondGzip(), theRequest, null, null);
}
}

View File

@ -167,7 +167,7 @@ public class GraphQLMethodBinding extends OperationMethodBinding {
}
// Write the response
Writer writer = theRequest.getResponse().getResponseWriter(statusCode, statusMessage, contentType, charset, respondGzip);
Writer writer = theRequest.getResponse().getResponseWriter(statusCode, contentType, charset, respondGzip);
writer.write(responseString);
writer.close();

View File

@ -21,14 +21,15 @@ package ca.uhn.fhir.rest.server.servlet;
*/
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.server.BaseParseAction;
import ca.uhn.fhir.rest.server.BaseRestfulResponse;
import ca.uhn.fhir.util.IoUtil;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseBinary;
import org.apache.commons.lang3.Validate;
import javax.annotation.Nonnull;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
@ -40,6 +41,9 @@ import java.util.zip.GZIPOutputStream;
public class ServletRestfulResponse extends BaseRestfulResponse<ServletRequestDetails> {
private Writer myWriter;
private OutputStream myOutputStream;
/**
* Constructor
*/
@ -47,24 +51,29 @@ public class ServletRestfulResponse extends BaseRestfulResponse<ServletRequestDe
super(servletRequestDetails);
}
@Nonnull
@Override
public OutputStream sendAttachmentResponse(IBaseBinary theBinary, int theStatusCode, String contentType) throws IOException {
public OutputStream getResponseOutputStream(int theStatusCode, String theContentType, Integer theContentLength) throws IOException {
Validate.isTrue(myWriter == null, "getResponseOutputStream() called multiple times" );
Validate.isTrue(myOutputStream == null, "getResponseOutputStream() called after getResponseWriter()" );
addHeaders();
HttpServletResponse theHttpResponse = getRequestDetails().getServletResponse();
theHttpResponse.setStatus(theStatusCode);
theHttpResponse.setContentType(contentType);
theHttpResponse.setCharacterEncoding(null);
if (theBinary.getContent() == null || theBinary.getContent().length == 0) {
return theHttpResponse.getOutputStream();
HttpServletResponse httpResponse = getRequestDetails().getServletResponse();
httpResponse.setStatus(theStatusCode);
httpResponse.setContentType(theContentType);
httpResponse.setCharacterEncoding(null);
if (theContentLength != null) {
httpResponse.setContentLength(theContentLength);
}
theHttpResponse.setContentLength(theBinary.getContent().length);
ServletOutputStream oos = theHttpResponse.getOutputStream();
oos.write(theBinary.getContent());
return oos;
myOutputStream = httpResponse.getOutputStream();
return myOutputStream;
}
@Nonnull
@Override
public Writer getResponseWriter(int theStatusCode, String theStatusMessage, String theContentType, String theCharset, boolean theRespondGzip) throws IOException {
public Writer getResponseWriter(int theStatusCode, String theContentType, String theCharset, boolean theRespondGzip) throws IOException {
Validate.isTrue(myOutputStream == null, "getResponseWriter() called after getResponseOutputStream()" );
addHeaders();
HttpServletResponse theHttpResponse = getRequestDetails().getServletResponse();
theHttpResponse.setCharacterEncoding(theCharset);
@ -72,16 +81,18 @@ public class ServletRestfulResponse extends BaseRestfulResponse<ServletRequestDe
theHttpResponse.setContentType(theContentType);
if (theRespondGzip) {
theHttpResponse.addHeader(Constants.HEADER_CONTENT_ENCODING, Constants.ENCODING_GZIP);
return new OutputStreamWriter(new GZIPOutputStream(theHttpResponse.getOutputStream()), StandardCharsets.UTF_8);
ServletOutputStream outputStream = theHttpResponse.getOutputStream();
myWriter = new OutputStreamWriter(new GZIPOutputStream(outputStream), StandardCharsets.UTF_8);
return myWriter;
}
// return new OutputStreamWriter(new GZIPOutputStream(theHttpResponse.getOutputStream()), StandardCharsets.UTF_8);
return theHttpResponse.getWriter();
myWriter = theHttpResponse.getWriter();
return myWriter;
}
private void addHeaders() {
HttpServletResponse theHttpResponse = getRequestDetails().getServletResponse();
getRequestDetails().getServer().addHeadersToResponse(theHttpResponse);
HttpServletResponse httpResponse = getRequestDetails().getServletResponse();
getRequestDetails().getServer().addHeadersToResponse(httpResponse);
for (Entry<String, List<String>> header : getHeaders().entrySet()) {
String key = header.getKey();
key = sanitizeHeaderField(key);
@ -91,27 +102,23 @@ public class ServletRestfulResponse extends BaseRestfulResponse<ServletRequestDe
// existing headers should be overridden
if (first) {
theHttpResponse.setHeader(key, value);
httpResponse.setHeader(key, value);
first = false;
} else {
theHttpResponse.addHeader(key, value);
httpResponse.addHeader(key, value);
}
}
}
}
@Override
public final Object commitResponse(@Nonnull Closeable theWriterOrOutputStream) {
IoUtil.closeQuietly(theWriterOrOutputStream);
return null;
}
static String sanitizeHeaderField(String theKey) {
return StringUtils.replaceChars(theKey, "\r\n", null);
}
@Override
public final Writer sendWriterResponse(int theStatus, String theContentType, String theCharset, Writer theWriter) {
return theWriter;
}
@Override
public Object returnResponse(BaseParseAction<?> outcome, int operationStatus, boolean allowPrefer, MethodOutcome response, String resourceName) throws IOException {
addHeaders();
return getRequestDetails().getServer().returnResponse(getRequestDetails(), outcome, operationStatus, allowPrefer, response, resourceName);
}
}

View File

@ -46,7 +46,7 @@ public class ServletRestfulResponseTest {
response.addHeader("Authorization", "Bearer");
response.addHeader("Cache-Control", "no-cache, no-store");
response.getResponseWriter(200, "Status", "text/plain", "UTF-8", false);
response.getResponseWriter(200, "text/plain", "UTF-8", false);
final InOrder orderVerifier = Mockito.inOrder(servletResponse);
orderVerifier.verify(servletResponse).setHeader(eq("Authorization"), eq("Basic"));

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
@ -11,54 +12,38 @@ import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.test.utilities.JettyUtil;
import ca.uhn.fhir.test.utilities.HttpClientExtension;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import ca.uhn.fhir.util.TestUtil;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class CustomTypeServerR4 {
private static CloseableHttpClient ourClient;
public class CustomTypeServerR4Test {
private static FhirContext ourCtx = FhirContext.forR4();
private static String ourLastConditionalUrl;
private static IdType ourLastId;
private static IdType ourLastIdParam;
private static boolean ourLastRequestWasSearch;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CustomTypeServerR4.class);
private static int ourPort;
private static Server ourServer;
@BeforeEach
public void before() {
ourLastId = null;
ourLastConditionalUrl = null;
ourLastIdParam = null;
ourLastRequestWasSearch = false;
}
private static FhirContext ourCtx = FhirContext.forR4Cached();
@RegisterExtension
private static RestfulServerExtension ourServer = new RestfulServerExtension(ourCtx)
.registerProvider(new PatientProvider())
.keepAliveBetweenTests();
@RegisterExtension
private static HttpClientExtension ourClient = new HttpClientExtension();
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CustomTypeServerR4Test.class);
@Test
public void testCreateWithIdInBody() throws Exception {
@ -67,8 +52,7 @@ public class CustomTypeServerR4 {
patient.setId("2");
patient.addIdentifier().setValue("002");
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient");
// httpPost.addHeader(Constants.HEADER_IF_NONE_EXIST, "Patient?identifier=system%7C001");
HttpPost httpPost = new HttpPost(ourServer.getBaseUrl() + "/Patient");
httpPost.setEntity(new StringEntity(ourCtx.newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
HttpResponse status = ourClient.execute(httpPost);
@ -87,8 +71,7 @@ public class CustomTypeServerR4 {
Patient patient = new Patient();
patient.addIdentifier().setValue("002");
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/2");
// httpPost.addHeader(Constants.HEADER_IF_NONE_EXIST, "Patient?identifier=system%7C001");
HttpPost httpPost = new HttpPost(ourServer.getBaseUrl() + "/Patient/2");
httpPost.setEntity(new StringEntity(ourCtx.newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
HttpResponse status = ourClient.execute(httpPost);
@ -100,7 +83,7 @@ public class CustomTypeServerR4 {
assertEquals(400, status.getStatusLine().getStatusCode());
OperationOutcome oo = ourCtx.newXmlParser().parseResource(OperationOutcome.class, responseContent);
assertEquals("Can not create resource with ID \"2\", ID must not be supplied on a create (POST) operation (use an HTTP PUT / update operation if you wish to supply an ID)", oo.getIssue().get(0).getDiagnostics());
assertEquals(Msg.code(365) + "Can not create resource with ID \"2\", ID must not be supplied on a create (POST) operation (use an HTTP PUT / update operation if you wish to supply an ID)", oo.getIssue().get(0).getDiagnostics());
}
@Test
@ -109,7 +92,7 @@ public class CustomTypeServerR4 {
Patient patient = new Patient();
patient.addIdentifier().setValue("002");
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/2");
HttpPost httpPost = new HttpPost(ourServer.getBaseUrl() + "/Patient/2");
httpPost.addHeader(Constants.HEADER_IF_NONE_EXIST, "Patient?identifier=system%7C001");
httpPost.setEntity(new StringEntity(ourCtx.newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
@ -122,45 +105,19 @@ public class CustomTypeServerR4 {
assertEquals(400, status.getStatusLine().getStatusCode());
OperationOutcome oo = ourCtx.newXmlParser().parseResource(OperationOutcome.class, responseContent);
assertEquals("Can not create resource with ID \"2\", ID must not be supplied on a create (POST) operation (use an HTTP PUT / update operation if you wish to supply an ID)", oo.getIssue().get(0).getDiagnostics());
assertEquals(Msg.code(365) + "Can not create resource with ID \"2\", ID must not be supplied on a create (POST) operation (use an HTTP PUT / update operation if you wish to supply an ID)", oo.getIssue().get(0).getDiagnostics());
}
@AfterAll
public static void afterClassClearContext() throws Exception {
JettyUtil.closeServer(ourServer);
TestUtil.randomizeLocaleAndTimezone();
}
@BeforeAll
public static void beforeClass() throws Exception {
ourServer = new Server(0);
PatientProvider patientProvider = new PatientProvider();
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer(ourCtx);
servlet.setResourceProviders(patientProvider);
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
JettyUtil.startServer(ourServer);
ourPort = JettyUtil.getPortForStartedServer(ourServer);
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setConnectionManager(connectionManager);
ourClient = builder.build();
}
public static class PatientProvider implements IResourceProvider {
@Create()
public MethodOutcome createPatient(@ResourceParam Patient thePatient, @ConditionalUrlParam String theConditional, @IdParam IdType theIdParam) {
ourLastConditionalUrl = theConditional;
ourLastId = thePatient.getIdElement();
ourLastIdParam = theIdParam;
return new MethodOutcome(new IdType("Patient/001/_history/002"));
}
@ -171,7 +128,6 @@ public class CustomTypeServerR4 {
@Search
public List<IResource> search(@OptionalParam(name = "foo") StringDt theString) {
ourLastRequestWasSearch = true;
return new ArrayList<>();
}

View File

@ -13,7 +13,8 @@ import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.test.utilities.JettyUtil;
import ca.uhn.fhir.test.utilities.HttpClientExtension;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import ca.uhn.fhir.util.TestUtil;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
@ -25,30 +26,15 @@ import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
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 org.hl7.fhir.r4.model.Binary;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CapabilityStatement;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.IntegerType;
import org.hl7.fhir.r4.model.MoneyQuantity;
import org.hl7.fhir.r4.model.OperationDefinition;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.model.OperationDefinition.OperationParameterUse;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.UnsignedIntType;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ -56,7 +42,6 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInRelativeOrder;
@ -68,10 +53,18 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
public class OperationServerR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationServerR4Test.class);
private static final String TEXT_HTML = "text/html";
private static CloseableHttpClient ourClient;
private static FhirContext ourCtx;
private static final FhirContext ourCtx = FhirContext.forR4Cached();
@RegisterExtension
private static final RestfulServerExtension ourServer = new RestfulServerExtension(ourCtx)
.registerProvider(new PatientProvider())
.registerProvider(new PlainProvider())
.withPagingProvider(new FifoMemoryPagingProvider(10).setDefaultPageSize(2))
.withDefaultResponseEncoding(EncodingEnum.XML);
@RegisterExtension
private static final HttpClientExtension ourClient = new HttpClientExtension();
private static IdType ourLastId;
private static String ourLastMethod;
private static StringType ourLastParam1;
@ -79,8 +72,6 @@ public class OperationServerR4Test {
private static List<StringType> ourLastParam3;
private static MoneyQuantity ourLastParamMoney1;
private static UnsignedIntType ourLastParamUnsignedInt1;
private static int ourPort;
private static Server ourServer;
private static IBaseResource ourNextResponse;
private static RestOperationTypeEnum ourLastRestOperation;
private IGenericClient myFhirClient;
@ -97,7 +88,7 @@ public class OperationServerR4Test {
ourNextResponse = null;
ourLastRestOperation = null;
myFhirClient = ourCtx.newRestfulGenericClient("http://localhost:" + ourPort);
myFhirClient = ourServer.getFhirClient();
}
@Test
@ -175,7 +166,7 @@ public class OperationServerR4Test {
patient.addIdentifier().setSystem("SYSTEM" ).setValue("VALUE" );
bundle.addEntry().setResource(patient);
HttpGet httpPost = new HttpGet("http://localhost:" + ourPort + "/Patient/$OP_TYPE_RETURNING_BUNDLE"
HttpGet httpPost = new HttpGet(ourServer.getBaseUrl() + "/Patient/$OP_TYPE_RETURNING_BUNDLE"
+ "?_pretty=true&_elements=identifier" );
try (CloseableHttpResponse status = ourClient.execute(httpPost)) {
@ -194,7 +185,7 @@ public class OperationServerR4Test {
public void testManualResponseWithPrimitiveParam() throws Exception {
// Try with a GET
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123/$manualResponseWithPrimitiveParam?path=THIS_IS_A_PATH");
HttpGet httpGet = new HttpGet(ourServer.getBaseUrl() + "/Patient/123/$manualResponseWithPrimitiveParam?path=THIS_IS_A_PATH" );
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
assertEquals(200, status.getStatusLine().getStatusCode());
String response = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
@ -210,7 +201,7 @@ public class OperationServerR4Test {
public void testInstanceEverythingGet() throws Exception {
// Try with a GET
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123/$everything");
HttpGet httpGet = new HttpGet(ourServer.getBaseUrl() + "/Patient/123/$everything" );
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
assertEquals(200, status.getStatusLine().getStatusCode());
String response = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
@ -226,7 +217,7 @@ public class OperationServerR4Test {
public void testInstanceOnPlainProvider() throws Exception {
// Try with a GET
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123/$OP_PLAIN_PROVIDER_ON_INSTANCE");
HttpGet httpGet = new HttpGet(ourServer.getBaseUrl() + "/Patient/123/$OP_PLAIN_PROVIDER_ON_INSTANCE" );
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
assertEquals(200, status.getStatusLine().getStatusCode());
String response = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
@ -240,7 +231,7 @@ public class OperationServerR4Test {
@Test
public void testInstanceEverythingHapiClient() {
ourCtx.newRestfulGenericClient("http://localhost:" + ourPort).operation().onInstance(new IdType("Patient/123")).named("$everything").withParameters(new Parameters()).execute();
ourCtx.newRestfulGenericClient(ourServer.getBaseUrl()).operation().onInstance(new IdType("Patient/123" )).named("$everything" ).withParameters(new Parameters()).execute();
assertEquals("instance $everything", ourLastMethod);
assertEquals("Patient/123", ourLastId.toUnqualifiedVersionless().getValue());
@ -251,7 +242,7 @@ public class OperationServerR4Test {
@Test
public void testInstanceVersionEverythingHapiClient() {
ourCtx
.newRestfulGenericClient("http://localhost:" + ourPort)
.newRestfulGenericClient(ourServer.getBaseUrl())
.operation()
.onInstanceVersion(new IdType("Patient/123/_history/456" ))
.named("$everything" )
@ -269,7 +260,7 @@ public class OperationServerR4Test {
String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(new Parameters());
// Try with a POST
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/123/$everything");
HttpPost httpPost = new HttpPost(ourServer.getBaseUrl() + "/Patient/123/$everything" );
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8" )));
HttpResponse status = ourClient.execute(httpPost);
@ -288,7 +279,7 @@ public class OperationServerR4Test {
byte[] bytes = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1};
ContentType contentType = ContentType.IMAGE_PNG;
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$manualInputAndOutput");
HttpPost httpPost = new HttpPost(ourServer.getBaseUrl() + "/Patient/$manualInputAndOutput" );
httpPost.setEntity(new ByteArrayEntity(bytes, contentType));
try (CloseableHttpResponse status = ourClient.execute(httpPost)) {
@ -307,7 +298,7 @@ public class OperationServerR4Test {
byte[] bytes = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1};
ContentType contentType = ContentType.IMAGE_PNG;
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$manualInputAndOutputWithParam?param1=value");
HttpPost httpPost = new HttpPost(ourServer.getBaseUrl() + "/Patient/$manualInputAndOutputWithParam?param1=value" );
httpPost.setEntity(new ByteArrayEntity(bytes, contentType));
try (CloseableHttpResponse status = ourClient.execute(httpPost)) {
@ -324,7 +315,7 @@ public class OperationServerR4Test {
@Test
public void testOperationCantUseGetIfItIsntIdempotent() throws Exception {
HttpGet httpPost = new HttpGet("http://localhost:" + ourPort + "/Patient/123/$OP_INSTANCE");
HttpGet httpPost = new HttpGet(ourServer.getBaseUrl() + "/Patient/123/$OP_INSTANCE" );
HttpResponse status = ourClient.execute(httpPost);
assertEquals(Constants.STATUS_HTTP_405_METHOD_NOT_ALLOWED, status.getStatusLine().getStatusCode());
@ -341,7 +332,7 @@ public class OperationServerR4Test {
p.addParameter().setName("PARAM1" ).setValue(new IntegerType(123));
String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/123/$OP_INSTANCE");
HttpPost httpPost = new HttpPost(ourServer.getBaseUrl() + "/Patient/123/$OP_INSTANCE" );
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8" )));
CloseableHttpResponse status = ourClient.execute(httpPost);
try {
@ -360,7 +351,7 @@ public class OperationServerR4Test {
p.addParameter().setName("PARAM2" ).setResource(new Patient().setActive(true));
String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/123/$OP_INSTANCE");
HttpPost httpPost = new HttpPost(ourServer.getBaseUrl() + "/Patient/123/$OP_INSTANCE" );
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8" )));
try (CloseableHttpResponse status = ourClient.execute(httpPost)) {
@ -380,7 +371,7 @@ public class OperationServerR4Test {
* Against type should fail
*/
httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$OP_INSTANCE");
httpPost = new HttpPost(ourServer.getBaseUrl() + "/Patient/$OP_INSTANCE" );
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8" )));
try (CloseableHttpResponse status = ourClient.execute(httpPost)) {
@ -399,7 +390,7 @@ public class OperationServerR4Test {
p.addParameter().setName("PARAM2" ).setResource(new Patient().setActive(true));
String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/123/$OP_INSTANCE_OR_TYPE");
HttpPost httpPost = new HttpPost(ourServer.getBaseUrl() + "/Patient/123/$OP_INSTANCE_OR_TYPE" );
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8" )));
HttpResponse status = ourClient.execute(httpPost);
@ -424,7 +415,7 @@ public class OperationServerR4Test {
p.addParameter().setName("PARAM2" ).setResource(new Patient().setActive(true));
String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$OP_INSTANCE_OR_TYPE");
HttpPost httpPost = new HttpPost(ourServer.getBaseUrl() + "/Patient/$OP_INSTANCE_OR_TYPE" );
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8" )));
CloseableHttpResponse status = ourClient.execute(httpPost);
@ -448,7 +439,7 @@ public class OperationServerR4Test {
p.addParameter().setName("PARAM2" ).setResource(new Patient().setActive(true));
String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/$OP_SERVER");
HttpPost httpPost = new HttpPost(ourServer.getBaseUrl() + "/$OP_SERVER" );
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8" )));
HttpResponse status = ourClient.execute(httpPost);
@ -471,7 +462,7 @@ public class OperationServerR4Test {
p.addParameter().setName("PARAM2" ).setResource(new Patient().setActive(true));
String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/$OP_SERVER_WITH_RAW_STRING");
HttpPost httpPost = new HttpPost(ourServer.getBaseUrl() + "/$OP_SERVER_WITH_RAW_STRING" );
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8" )));
HttpResponse status = ourClient.execute(httpPost);
@ -494,7 +485,7 @@ public class OperationServerR4Test {
p.addParameter().setName("PARAM2" ).setResource(new Patient().setActive(true));
String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$OP_TYPE");
HttpPost httpPost = new HttpPost(ourServer.getBaseUrl() + "/Patient/$OP_TYPE" );
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8" )));
HttpResponse status = ourClient.execute(httpPost);
@ -517,7 +508,7 @@ public class OperationServerR4Test {
p.addParameter().setName("PARAM2" ).setResource(new Patient().setActive(true));
String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$OP_TYPE_RET_BUNDLE");
HttpPost httpPost = new HttpPost(ourServer.getBaseUrl() + "/Patient/$OP_TYPE_RET_BUNDLE" );
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8" )));
HttpResponse status = ourClient.execute(httpPost);
@ -535,7 +526,7 @@ public class OperationServerR4Test {
@Test
public void testOperationWithBundleProviderResponse() throws Exception {
HttpGet httpPost = new HttpGet("http://localhost:" + ourPort + "/$OP_SERVER_BUNDLE_PROVIDER?_pretty=true");
HttpGet httpPost = new HttpGet(ourServer.getBaseUrl() + "/$OP_SERVER_BUNDLE_PROVIDER?_pretty=true" );
HttpResponse status = ourClient.execute(httpPost);
assertEquals(200, status.getStatusLine().getStatusCode());
@ -548,7 +539,7 @@ public class OperationServerR4Test {
@Test
public void testOperationWithGetUsingParams() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/$OP_TYPE?PARAM1=PARAM1val");
HttpGet httpGet = new HttpGet(ourServer.getBaseUrl() + "/Patient/$OP_TYPE?PARAM1=PARAM1val" );
HttpResponse status = ourClient.execute(httpGet);
assertEquals(200, status.getStatusLine().getStatusCode());
@ -565,7 +556,7 @@ public class OperationServerR4Test {
@Test
public void testOperationWithGetUsingParamsFailsWithNonPrimitive() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/$OP_TYPE?PARAM1=PARAM1val&PARAM2=foo");
HttpGet httpGet = new HttpGet(ourServer.getBaseUrl() + "/Patient/$OP_TYPE?PARAM1=PARAM1val&PARAM2=foo" );
HttpResponse status = ourClient.execute(httpGet);
assertEquals(405, status.getStatusLine().getStatusCode());
@ -584,7 +575,7 @@ public class OperationServerR4Test {
p.addParameter().setName("PARAM3" ).setValue(new StringType("PARAM3val2" ));
String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/$OP_SERVER_LIST_PARAM");
HttpPost httpPost = new HttpPost(ourServer.getBaseUrl() + "/$OP_SERVER_LIST_PARAM" );
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8" )));
HttpResponse status = ourClient.execute(httpPost);
@ -609,7 +600,7 @@ public class OperationServerR4Test {
p.addParameter().setName("PARAM1" ).setValue(new IntegerType("123" ));
String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$OP_PROFILE_DT");
HttpPost httpPost = new HttpPost(ourServer.getBaseUrl() + "/Patient/$OP_PROFILE_DT" );
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8" )));
HttpResponse status = ourClient.execute(httpPost);
@ -630,7 +621,7 @@ public class OperationServerR4Test {
p.addParameter().setName("PARAM1" ).setValue(money);
String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$OP_PROFILE_DT2");
HttpPost httpPost = new HttpPost(ourServer.getBaseUrl() + "/Patient/$OP_PROFILE_DT2" );
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8" )));
HttpResponse status = ourClient.execute(httpPost);
@ -645,7 +636,7 @@ public class OperationServerR4Test {
@Test
public void testOperationWithProfileDatatypeUrl() throws Exception {
HttpGet httpPost = new HttpGet("http://localhost:" + ourPort + "/Patient/$OP_PROFILE_DT?PARAM1=123");
HttpGet httpPost = new HttpGet(ourServer.getBaseUrl() + "/Patient/$OP_PROFILE_DT?PARAM1=123" );
HttpResponse status = ourClient.execute(httpPost);
assertEquals(200, status.getStatusLine().getStatusCode());
@ -662,7 +653,7 @@ public class OperationServerR4Test {
p.addParameter().setName("PARAM2" ).setResource(new Patient().setActive(true));
String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$OP_TYPE");
HttpPost httpPost = new HttpPost(ourServer.getBaseUrl() + "/Patient/$OP_TYPE" );
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8" )));
HttpResponse status = ourClient.execute(httpPost);
@ -678,7 +669,7 @@ public class OperationServerR4Test {
@Test
public void testReadWithOperations() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123");
HttpGet httpGet = new HttpGet(ourServer.getBaseUrl() + "/Patient/123" );
HttpResponse status = ourClient.execute(httpGet);
assertEquals(200, status.getStatusLine().getStatusCode());
@ -689,7 +680,7 @@ public class OperationServerR4Test {
@Test
public void testReturnBinaryWithAcceptFhir() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/$binaryop");
HttpGet httpGet = new HttpGet(ourServer.getBaseUrl() + "/$binaryop?_pretty=false" );
httpGet.addHeader(Constants.HEADER_ACCEPT, Constants.HEADER_ACCEPT_VALUE_XML_OR_JSON_NON_LEGACY);
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
assertEquals(200, status.getStatusLine().getStatusCode());
@ -702,7 +693,7 @@ public class OperationServerR4Test {
@Test
public void testReturnBinaryWithAcceptHtml() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/$binaryop");
HttpGet httpGet = new HttpGet(ourServer.getBaseUrl() + "/$binaryop" );
httpGet.addHeader(Constants.HEADER_ACCEPT, TEXT_HTML);
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
assertEquals(200, status.getStatusLine().getStatusCode());
@ -1001,36 +992,7 @@ public class OperationServerR4Test {
@AfterAll
public static void afterClassClearContext() throws Exception {
JettyUtil.closeServer(ourServer);
TestUtil.randomizeLocaleAndTimezone();
}
@BeforeAll
public static void beforeClass() throws Exception {
ourCtx = FhirContext.forR4();
ourServer = new Server(0);
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer(ourCtx);
servlet.setDefaultResponseEncoding(EncodingEnum.XML);
servlet.setPagingProvider(new FifoMemoryPagingProvider(10).setDefaultPageSize(2));
servlet.setFhirContext(ourCtx);
PlainProvider plainProvider = new PlainProvider();
PatientProvider patientProvider = new PatientProvider();
servlet.registerProviders(patientProvider, plainProvider);
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
JettyUtil.startServer(ourServer);
ourPort = JettyUtil.getPortForStartedServer(ourServer);
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setConnectionManager(connectionManager);
ourClient = builder.build();
}
}

View File

@ -0,0 +1,199 @@
package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.test.utilities.HttpClientExtension;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import com.helger.commons.collection.iterate.EmptyEnumeration;
import org.apache.commons.collections4.iterators.IteratorEnumeration;
import org.apache.commons.lang3.RandomStringUtils;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import javax.annotation.Nonnull;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class ServerConcurrencyTest {
private static final FhirContext ourCtx = FhirContext.forR4Cached();
private static final Logger ourLog = LoggerFactory.getLogger(ServerConcurrencyTest.class);
@RegisterExtension
private static final RestfulServerExtension ourServer = new RestfulServerExtension(ourCtx)
.registerProvider(new MyPatientProvider());
@RegisterExtension
private final HttpClientExtension myHttpClient = new HttpClientExtension();
@Mock
private HttpServletRequest myRequest;
@Mock
private HttpServletResponse myResponse;
@Mock
private PrintWriter myWriter;
private HashMap<String, String> myHeaders;
@Test
public void testExceptionClosingInputStream() throws IOException {
initRequestMocks();
DelegatingServletInputStream inputStream = createMockPatientBodyServletInputStream();
inputStream.setExceptionOnClose(true);
when(myRequest.getInputStream()).thenReturn(inputStream);
when(myResponse.getWriter()).thenReturn(myWriter);
assertDoesNotThrow(() ->
ourServer.getRestfulServer().handleRequest(RequestTypeEnum.POST, myRequest, myResponse)
);
}
@Test
public void testExceptionClosingOutputStream() throws IOException {
initRequestMocks();
when(myRequest.getInputStream()).thenReturn(createMockPatientBodyServletInputStream());
when(myResponse.getWriter()).thenReturn(myWriter);
// Throw an exception when the stream is closed
doThrow(new EOFException()).when(myWriter).close();
assertDoesNotThrow(() ->
ourServer.getRestfulServer().handleRequest(RequestTypeEnum.POST, myRequest, myResponse)
);
}
private void initRequestMocks() {
myHeaders = new HashMap<>();
myHeaders.put(Constants.HEADER_CONTENT_TYPE, Constants.CT_FHIR_JSON_NEW);
when(myRequest.getRequestURI()).thenReturn("/Patient");
when(myRequest.getRequestURL()).thenReturn(new StringBuffer(ourServer.getBaseUrl() + "/Patient"));
when(myRequest.getHeader(any())).thenAnswer(t -> {
String header = t.getArgument(0, String.class);
String value = myHeaders.get(header);
ourLog.info("Request for header '{}' produced: {}", header, value);
return value;
});
when(myRequest.getHeaders(any())).thenAnswer(t -> {
String header = t.getArgument(0, String.class);
String value = myHeaders.get(header);
ourLog.info("Request for header '{}' produced: {}", header, value);
if (value != null) {
return new IteratorEnumeration<>(Collections.singleton(value).iterator());
}
return new EmptyEnumeration<>();
});
}
/**
* Based on the class from Spring Test with the same name
*/
public static class DelegatingServletInputStream extends ServletInputStream {
private final InputStream mySourceStream;
private boolean myFinished = false;
private boolean myExceptionOnClose = false;
public DelegatingServletInputStream(InputStream sourceStream) {
Assert.notNull(sourceStream, "Source InputStream must not be null");
this.mySourceStream = sourceStream;
}
public void setExceptionOnClose(boolean theExceptionOnClose) {
myExceptionOnClose = theExceptionOnClose;
}
@Override
public int read() throws IOException {
int data = this.mySourceStream.read();
if (data == -1) {
this.myFinished = true;
}
return data;
}
@Override
public int available() throws IOException {
return this.mySourceStream.available();
}
@Override
public void close() throws IOException {
super.close();
this.mySourceStream.close();
if (myExceptionOnClose) {
throw new IOException("Failed!");
}
}
@Override
public boolean isFinished() {
return this.myFinished;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
}
@SuppressWarnings("unused")
public static class MyPatientProvider implements IResourceProvider {
@Create
public MethodOutcome create(@ResourceParam Patient thePatient) throws InterruptedException {
OperationOutcome oo = new OperationOutcome();
oo.addIssue().setDiagnostics(RandomStringUtils.randomAlphanumeric(1000));
return new MethodOutcome()
.setId(new IdType("Patient/A"))
.setOperationOutcome(oo);
}
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
}
@Nonnull
public static DelegatingServletInputStream createMockPatientBodyServletInputStream() {
Patient input = new Patient();
input.addName().setFamily(RandomStringUtils.randomAlphanumeric(100000));
String patient = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(input);
ByteArrayInputStream bais = new ByteArrayInputStream(patient.getBytes(StandardCharsets.UTF_8));
return new DelegatingServletInputStream(bais);
}
}

View File

@ -2,11 +2,14 @@ package ca.uhn.fhir.rest.server.interceptor;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.param.StringParam;
@ -19,8 +22,11 @@ import ca.uhn.fhir.rest.server.interceptor.consent.IConsentService;
import ca.uhn.fhir.rest.server.provider.HashMapResourceProvider;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.test.utilities.HttpClientExtension;
import ca.uhn.fhir.test.utilities.LoggingExtension;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import com.google.common.base.Charsets;
import com.helger.commons.collection.iterate.EmptyEnumeration;
import org.apache.commons.collections4.iterators.IteratorEnumeration;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
@ -41,16 +47,28 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.util.Assert;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
@ -65,13 +83,15 @@ public class ConsentInterceptorTest {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ConsentInterceptorTest.class);
@RegisterExtension
private final HttpClientExtension myClient = new HttpClientExtension();
@RegisterExtension
private final LoggingExtension myLoggingExtension = new LoggingExtension();
private static final FhirContext ourCtx = FhirContext.forR4Cached();
private int myPort;
private static final DummyPatientResourceProvider ourPatientProvider = new DummyPatientResourceProvider(ourCtx);
private static final DummySystemProvider ourSystemProvider = new DummySystemProvider();
@RegisterExtension
private static RestfulServerExtension ourServer = new RestfulServerExtension(ourCtx)
private static final RestfulServerExtension ourServer = new RestfulServerExtension(ourCtx)
.registerProvider(ourPatientProvider)
.registerProvider(ourSystemProvider)
.withPagingProvider(new FifoMemoryPagingProvider(10));
@ -129,8 +149,8 @@ public class ConsentInterceptorTest {
ourLog.info("Response: {}", responseContent);
}
verify(myConsentSvc, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, timeout(2000).times(0)).completeOperationFailure(any(), any(), any());
}
@Test
@ -243,10 +263,10 @@ public class ConsentInterceptorTest {
ourLog.info("Response: {}", responseContent);
}
verify(myConsentSvc, times(0)).canSeeResource(any(), any(), any());
verify(myConsentSvc, times(0)).willSeeResource(any(), any(), any());
verify(myConsentSvc, times(0)).startOperation(any(), any());
verify(myConsentSvc, times(2)).completeOperationSuccess(any(), any());
verify(myConsentSvc, timeout(2000).times(0)).canSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(0)).willSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(0)).startOperation(any(), any());
verify(myConsentSvc, timeout(2000).times(2)).completeOperationSuccess(any(), any());
}
@ -268,12 +288,12 @@ public class ConsentInterceptorTest {
assertThat(responseContent, containsString("PTA"));
}
verify(myConsentSvc, times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, times(0)).canSeeResource(any(), any(), any());
verify(myConsentSvc, times(3)).willSeeResource(any(), any(), any());
verify(myConsentSvc, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(1)).startOperation(any(), any());
verify(myConsentSvc, timeout(2000).times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, timeout(2000).times(0)).canSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(3)).willSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, timeout(2000).times(0)).completeOperationFailure(any(), any(), any());
verifyNoMoreInteractions(myConsentSvc);
}
@ -296,12 +316,12 @@ public class ConsentInterceptorTest {
assertThat(responseContent, containsString("PTA"));
}
verify(myConsentSvc, times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, times(2)).canSeeResource(any(), any(), any());
verify(myConsentSvc, times(3)).willSeeResource(any(), any(), any());
verify(myConsentSvc, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(1)).startOperation(any(), any());
verify(myConsentSvc, timeout(2000).times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, timeout(2000).times(2)).canSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(3)).willSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, timeout(2000).times(0)).completeOperationFailure(any(), any(), any());
verifyNoMoreInteractions(myConsentSvc);
}
@ -329,7 +349,7 @@ public class ConsentInterceptorTest {
}
verify(myConsentSvc, timeout(10000).times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, timeout(2000).times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, timeout(10000).times(2)).canSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(10000).times(3)).willSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(10000).times(1)).completeOperationSuccess(any(), any());
@ -357,12 +377,12 @@ public class ConsentInterceptorTest {
assertNull(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE));
}
verify(myConsentSvc, times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, times(2)).canSeeResource(any(), any(), any());
verify(myConsentSvc, times(3)).willSeeResource(any(), any(), any()); // the two patients + the bundle
verify(myConsentSvc, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(1)).startOperation(any(), any());
verify(myConsentSvc, timeout(2000).times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, timeout(2000).times(2)).canSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(3)).willSeeResource(any(), any(), any()); // the two patients + the bundle
verify(myConsentSvc, timeout(2000).times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, timeout(2000).times(0)).completeOperationFailure(any(), any(), any());
verifyNoMoreInteractions(myConsentSvc);
}
@ -398,11 +418,11 @@ public class ConsentInterceptorTest {
}
verify(myConsentSvc, timeout(1000).times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, timeout(2000).times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, timeout(1000).times(2)).canSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(1000).times(3)).willSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(1000).times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(0)).completeOperationFailure(any(), any(), any());
verifyNoMoreInteractions(myConsentSvc);
}
@ -438,12 +458,12 @@ public class ConsentInterceptorTest {
assertEquals("PTB", response.getEntry().get(1).getResource().getIdElement().getIdPart());
}
verify(myConsentSvc, times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, times(2)).canSeeResource(any(), any(), any());
verify(myConsentSvc, times(4)).willSeeResource(any(), any(), any());
verify(myConsentSvc, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(1)).startOperation(any(), any());
verify(myConsentSvc, timeout(2000).times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, timeout(2000).times(2)).canSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(4)).willSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, timeout(2000).times(0)).completeOperationFailure(any(), any(), any());
verifyNoMoreInteractions(myConsentSvc);
}
@ -476,12 +496,12 @@ public class ConsentInterceptorTest {
assertEquals("PTB", response.getEntry().get(1).getResource().getIdElement().getIdPart());
}
verify(myConsentSvc, times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, times(2)).canSeeResource(any(), any(), any());
verify(myConsentSvc, times(3)).willSeeResource(any(), any(), any());
verify(myConsentSvc, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(1)).startOperation(any(), any());
verify(myConsentSvc, timeout(2000).times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, timeout(2000).times(2)).canSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(3)).willSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, timeout(2000).times(0)).completeOperationFailure(any(), any(), any());
verifyNoMoreInteractions(myConsentSvc);
}
@ -529,7 +549,7 @@ public class ConsentInterceptorTest {
assertEquals("REPLACEMENT", ((Patient)response.getEntry().get(0).getResource()).getIdentifierFirstRep().getSystem());
}
verify(myConsentSvc, times(1)).startOperation(any(), any());
verify(myConsentSvc, timeout(2000).times(1)).startOperation(any(), any());
}
@ -554,18 +574,18 @@ public class ConsentInterceptorTest {
assertNull(response.getTotalElement().getValue());
assertEquals(0, response.getEntry().size());
verify(myConsentSvc, times(1)).startOperation(any(), any());
verify(myConsentSvc2, times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc2, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, times(1)).canSeeResource(any(), any(), any());
verify(myConsentSvc2, times(0)).canSeeResource(any(), any(), any());
verify(myConsentSvc, times(1)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc2, times(1)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc2, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc2, times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(1)).startOperation(any(), any());
verify(myConsentSvc2, timeout(2000).times(1)).startOperation(any(), any());
verify(myConsentSvc, timeout(2000).times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc2, timeout(2000).times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, timeout(2000).times(1)).canSeeResource(any(), any(), any());
verify(myConsentSvc2, timeout(2000).times(0)).canSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(1)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc2, timeout(2000).times(1)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc, timeout(2000).times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc2, timeout(2000).times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, timeout(2000).times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc2, timeout(2000).times(0)).completeOperationFailure(any(), any(), any());
verifyNoMoreInteractions(myConsentSvc);
verifyNoMoreInteractions(myConsentSvc2);
}
@ -602,18 +622,18 @@ public class ConsentInterceptorTest {
assertThat(responseContent, not(containsString("\"entry\"")));
}
verify(myConsentSvc, times(1)).startOperation(any(), any());
verify(myConsentSvc2, times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc2, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, times(0)).canSeeResource(any(), any(), any());
verify(myConsentSvc2, times(2)).canSeeResource(any(), any(), any());
verify(myConsentSvc, times(3)).willSeeResource(any(), any(), any());
verify(myConsentSvc2, times(2)).willSeeResource(any(), any(), any());
verify(myConsentSvc, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc2, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc2, times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(1)).startOperation(any(), any());
verify(myConsentSvc2, timeout(2000).times(1)).startOperation(any(), any());
verify(myConsentSvc, timeout(2000).times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc2, timeout(2000).times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, timeout(2000).times(0)).canSeeResource(any(), any(), any());
verify(myConsentSvc2, timeout(2000).times(2)).canSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(3)).willSeeResource(any(), any(), any());
verify(myConsentSvc2, timeout(2000).times(2)).willSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc2, timeout(2000).times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, timeout(2000).times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc2, timeout(2000).times(0)).completeOperationFailure(any(), any(), any());
verifyNoMoreInteractions(myConsentSvc);
}
@ -638,18 +658,18 @@ public class ConsentInterceptorTest {
assertNull(response.getTotalElement().getValue());
assertEquals(1, response.getEntry().size());
verify(myConsentSvc, times(1)).startOperation(any(), any());
verify(myConsentSvc2, times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc2, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, times(1)).canSeeResource(any(), any(), any());
verify(myConsentSvc2, times(0)).canSeeResource(any(), any(), any());
verify(myConsentSvc, times(1)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc2, times(1)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc2, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc2, times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(1)).startOperation(any(), any());
verify(myConsentSvc2, timeout(2000).times(1)).startOperation(any(), any());
verify(myConsentSvc, timeout(2000).times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc2, timeout(2000).times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, timeout(2000).times(1)).canSeeResource(any(), any(), any());
verify(myConsentSvc2, timeout(2000).times(0)).canSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(1)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc2, timeout(2000).times(1)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc, timeout(2000).times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc2, timeout(2000).times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, timeout(2000).times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc2, timeout(2000).times(0)).completeOperationFailure(any(), any(), any());
verifyNoMoreInteractions(myConsentSvc);
verifyNoMoreInteractions(myConsentSvc2);
}
@ -676,18 +696,18 @@ public class ConsentInterceptorTest {
assertNull(response.getTotalElement().getValue());
assertEquals(0, response.getEntry().size());
verify(myConsentSvc, times(1)).startOperation(any(), any());
verify(myConsentSvc2, times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc2, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, times(1)).canSeeResource(any(), any(), any());
verify(myConsentSvc2, times(1)).canSeeResource(any(), any(), any());
verify(myConsentSvc, times(1)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc2, times(1)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc2, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc2, times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(1)).startOperation(any(), any());
verify(myConsentSvc2, timeout(2000).times(1)).startOperation(any(), any());
verify(myConsentSvc, timeout(2000).times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc2, timeout(2000).times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, timeout(2000).times(1)).canSeeResource(any(), any(), any());
verify(myConsentSvc2, timeout(2000).times(1)).canSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(1)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc2, timeout(2000).times(1)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc, timeout(2000).times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc2, timeout(2000).times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, timeout(2000).times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc2, timeout(2000).times(0)).completeOperationFailure(any(), any(), any());
verifyNoMoreInteractions(myConsentSvc);
verifyNoMoreInteractions(myConsentSvc2);
}
@ -719,22 +739,111 @@ public class ConsentInterceptorTest {
Patient patient = (Patient) response.getEntry().get(0).getResource();
assertEquals(2, patient.getIdentifier().size());
verify(myConsentSvc, times(1)).startOperation(any(), any());
verify(myConsentSvc2, times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc2, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, times(1)).canSeeResource(any(), any(), any());
verify(myConsentSvc2, times(1)).canSeeResource(any(), any(), any());
verify(myConsentSvc, times(2)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc2, times(2)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc2, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc2, times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(1)).startOperation(any(), any());
verify(myConsentSvc2, timeout(2000).times(1)).startOperation(any(), any());
verify(myConsentSvc, timeout(2000).times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc2, timeout(2000).times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, timeout(2000).times(1)).canSeeResource(any(), any(), any());
verify(myConsentSvc2, timeout(2000).times(1)).canSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(2000).times(2)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc2, timeout(2000).times(2)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc, timeout(2000).times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc2, timeout(2000).times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, timeout(2000).times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc2, timeout(2000).times(0)).completeOperationFailure(any(), any(), any());
verifyNoMoreInteractions(myConsentSvc);
verifyNoMoreInteractions(myConsentSvc2);
}
@Mock
private HttpServletRequest myRequest;
@Mock
private HttpServletResponse myResponse;
@Mock
private PrintWriter myWriter;
private HashMap<String, String> myHeaders;
private void initRequestMocks() {
myHeaders = new HashMap<>();
myHeaders.put(Constants.HEADER_CONTENT_TYPE, Constants.CT_FHIR_JSON_NEW);
when(myRequest.getRequestURI()).thenReturn("/Patient");
when(myRequest.getRequestURL()).thenReturn(new StringBuffer(ourServer.getBaseUrl() + "/Patient"));
when(myRequest.getHeader(any())).thenAnswer(t -> {
String header = t.getArgument(0, String.class);
String value = myHeaders.get(header);
ourLog.info("Request for header '{}' produced: {}", header, value);
return value;
});
when(myRequest.getHeaders(any())).thenAnswer(t -> {
String header = t.getArgument(0, String.class);
String value = myHeaders.get(header);
ourLog.info("Request for header '{}' produced: {}", header, value);
if (value != null) {
return new IteratorEnumeration<>(Collections.singleton(value).iterator());
}
return new EmptyEnumeration<>();
});
}
/**
* Based on the class from Spring Test with the same name
*/
public static class DelegatingServletInputStream extends ServletInputStream {
private final InputStream mySourceStream;
private boolean myFinished = false;
public void setExceptionOnClose(boolean theExceptionOnClose) {
myExceptionOnClose = theExceptionOnClose;
}
private boolean myExceptionOnClose = false;
public DelegatingServletInputStream(InputStream sourceStream) {
Assert.notNull(sourceStream, "Source InputStream must not be null");
this.mySourceStream = sourceStream;
}
@Override
public int read() throws IOException {
int data = this.mySourceStream.read();
if (data == -1) {
this.myFinished = true;
}
return data;
}
@Override
public int available() throws IOException {
return this.mySourceStream.available();
}
@Override
public void close() throws IOException {
super.close();
this.mySourceStream.close();
if (myExceptionOnClose) {
throw new IOException("Failed!");
}
}
@Override
public boolean isFinished() {
return this.myFinished;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
}
@Test
public void testOutcomeException() throws IOException {
when(myConsentSvc.startOperation(any(), any())).thenReturn(ConsentOutcome.PROCEED);
@ -747,8 +856,8 @@ public class ConsentInterceptorTest {
ourLog.info("Response: {}", responseContent);
}
verify(myConsentSvc, times(0)).completeOperationSuccess(any(), any());
verify(myConsentSvc, times(1)).completeOperationFailure(any(), myExceptionCaptor.capture(), any());
verify(myConsentSvc, timeout(2000).times(0)).completeOperationSuccess(any(), any());
verify(myConsentSvc, timeout(2000).times(1)).completeOperationFailure(any(), myExceptionCaptor.capture(), any());
assertEquals(Msg.code(389) + "Failed to call access method: java.lang.NullPointerException: A MESSAGE", myExceptionCaptor.getValue().getMessage());
}

View File

@ -15,7 +15,9 @@ import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.test.utilities.HttpClientExtension;
import ca.uhn.fhir.test.utilities.JettyUtil;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import ca.uhn.fhir.util.TestUtil;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
@ -35,6 +37,7 @@ import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.opentest4j.AssertionFailedError;
import java.io.IOException;
@ -46,36 +49,42 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
public class ExceptionHandlingInterceptorTest {
private static ExceptionHandlingInterceptor myInterceptor;
private static final String OPERATION_OUTCOME_DETAILS = "OperationOutcomeDetails";
private static CloseableHttpClient ourClient;
private static FhirContext ourCtx = FhirContext.forR4();
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExceptionHandlingInterceptorTest.class);
@RegisterExtension
private static final RestfulServerExtension ourServer = new RestfulServerExtension(ourCtx)
.withDefaultResponseEncoding(EncodingEnum.XML)
.registerProvider(new DummyPatientResourceProvider());
@RegisterExtension
private static final HttpClientExtension ourClient = new HttpClientExtension();
private ExceptionHandlingInterceptor myInterceptor;
private static final String OPERATION_OUTCOME_DETAILS = "OperationOutcomeDetails";
private static Class<? extends Exception> ourExceptionType;
private static boolean ourGenerateOperationOutcome;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExceptionHandlingInterceptorTest.class);
private static int ourPort;
private static Server ourServer;
private static RestfulServer servlet;
@BeforeEach
public void before() {
public void beforeEach() {
ourGenerateOperationOutcome = false;
ourExceptionType=null;
myInterceptor = new ExceptionHandlingInterceptor();
myInterceptor.setReturnStackTracesForExceptionTypes(Throwable.class);
ourServer.registerInterceptor(myInterceptor);
}
@Test
public void testInternalError() throws Exception {
myInterceptor.setReturnStackTracesForExceptionTypes(Throwable.class);
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?throwInternalError=aaa");
HttpGet httpGet = new HttpGet(ourServer.getBaseUrl() + "/Patient?throwInternalError=aaa");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info(responseContent);
assertEquals(500, status.getStatusLine().getStatusCode());
OperationOutcome oo = (OperationOutcome) servlet.getFhirContext().newXmlParser().parseResource(responseContent);
OperationOutcome oo = (OperationOutcome) ourCtx.newXmlParser().parseResource(responseContent);
assertThat(oo.getIssueFirstRep().getDiagnosticsElement().getValue(), StringContains.containsString("Exception Text"));
assertThat(oo.getIssueFirstRep().getDiagnosticsElement().getValue(), (StringContains.containsString("InternalErrorException: Exception Text")));
}
@ -86,36 +95,34 @@ public class ExceptionHandlingInterceptorTest {
//Given: We have an interceptor which causes a failure after the response output stream has been started.
ProblemGeneratingInterceptor interceptor = new ProblemGeneratingInterceptor();
servlet.registerInterceptor(interceptor);
myInterceptor.setReturnStackTracesForExceptionTypes(Throwable.class);
ourServer.registerInterceptor(interceptor);
//When: We make a request to the server, triggering this exception to be thrown on an otherwise successful request
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?succeed=true");
HttpGet httpGet = new HttpGet(ourServer.getBaseUrl() + "/Patient?succeed=true");
httpGet.setHeader("Accept-encoding", "gzip");
HttpResponse status = ourClient.execute(httpGet);
servlet.unregisterInterceptor(interceptor);
ourServer.unregisterInterceptor(interceptor);
//Then: This should still return an OperationOutcome, and not explode with an HTML IllegalState response.
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info(responseContent);
assertEquals(500, status.getStatusLine().getStatusCode());
OperationOutcome oo = (OperationOutcome) servlet.getFhirContext().newXmlParser().parseResource(responseContent);
ourLog.info(servlet.getFhirContext().newXmlParser().encodeResourceToString(oo));
OperationOutcome oo = (OperationOutcome) ourCtx.newXmlParser().parseResource(responseContent);
ourLog.info(ourCtx.newXmlParser().encodeResourceToString(oo));
assertThat(oo.getIssueFirstRep().getDiagnosticsElement().getValue(), StringContains.containsString("Simulated IOException"));
}
@Test
public void testInternalErrorFormatted() throws Exception {
myInterceptor.setReturnStackTracesForExceptionTypes(Throwable.class);
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?throwInternalError=aaa&_format=true");
HttpGet httpGet = new HttpGet(ourServer.getBaseUrl() + "/Patient?throwInternalError=aaa&_format=true");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info(responseContent);
assertEquals(500, status.getStatusLine().getStatusCode());
OperationOutcome oo = (OperationOutcome) servlet.getFhirContext().newXmlParser().parseResource(responseContent);
OperationOutcome oo = (OperationOutcome) ourCtx.newXmlParser().parseResource(responseContent);
assertThat(oo.getIssueFirstRep().getDiagnosticsElement().getValue(), StringContains.containsString("Exception Text"));
assertThat(oo.getIssueFirstRep().getDiagnosticsElement().getValue(), (StringContains.containsString("InternalErrorException: Exception Text")));
}
@ -125,33 +132,8 @@ public class ExceptionHandlingInterceptorTest {
@AfterAll
public static void afterClassClearContext() throws Exception {
JettyUtil.closeServer(ourServer);
TestUtil.randomizeLocaleAndTimezone();
}
@BeforeAll
public static void beforeClass() throws Exception {
ourServer = new Server(0);
DummyPatientResourceProvider patientProvider = new DummyPatientResourceProvider();
ServletHandler proxyHandler = new ServletHandler();
servlet = new RestfulServer(ourCtx);
servlet.setResourceProviders(patientProvider);
servlet.setDefaultResponseEncoding(EncodingEnum.XML);
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
JettyUtil.startServer(ourServer);
ourPort = JettyUtil.getPortForStartedServer(ourServer);
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setConnectionManager(connectionManager);
ourClient = builder.build();
myInterceptor = new ExceptionHandlingInterceptor();
servlet.registerInterceptor(myInterceptor);
}
public static class ProblemGeneratingInterceptor {
@Hook(Pointcut.SERVER_OUTGOING_WRITER_CREATED)

View File

@ -25,6 +25,7 @@ import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.server.IPagingProvider;
@ -45,12 +46,12 @@ public class RestfulServerExtension extends BaseJettyServerExtension<RestfulServ
private FhirContext myFhirContext;
private List<Object> myProviders = new ArrayList<>();
private FhirVersionEnum myFhirVersion;
private IGenericClient myFhirClient;
private RestfulServer myServlet;
private List<Consumer<RestfulServer>> myConsumers = new ArrayList<>();
private Map<String, Object> myRunningServerUserData = new HashMap<>();
private ServerValidationModeEnum myServerValidationMode = ServerValidationModeEnum.NEVER;
private IPagingProvider myPagingProvider;
/**
* Constructor
*/
@ -83,7 +84,6 @@ public class RestfulServerExtension extends BaseJettyServerExtension<RestfulServ
myFhirContext.getRestfulClientFactory().setSocketTimeout((int) (500 * DateUtils.MILLIS_PER_SECOND));
myFhirContext.getRestfulClientFactory().setServerValidationMode(myServerValidationMode);
myFhirClient = myFhirContext.newRestfulGenericClient(getBaseUrl());
}
@Override
@ -110,7 +110,6 @@ public class RestfulServerExtension extends BaseJettyServerExtension<RestfulServ
if (!isRunning()) {
return;
}
myFhirClient = null;
myRunningServerUserData.clear();
myPagingProvider = null;
myServlet = null;
@ -123,8 +122,11 @@ public class RestfulServerExtension extends BaseJettyServerExtension<RestfulServ
}
}
/**
* Creates a new client for each callof this method
*/
public IGenericClient getFhirClient() {
return myFhirClient;
return myFhirContext.newRestfulGenericClient(getBaseUrl());
}
public FhirContext getFhirContext() {
@ -210,4 +212,8 @@ public class RestfulServerExtension extends BaseJettyServerExtension<RestfulServ
public RestfulServerExtension registerAnonymousInterceptor(Pointcut thePointcut, IAnonymousInterceptor theInterceptor) {
return withServer(t -> t.getInterceptorService().registerAnonymousInterceptor(thePointcut, theInterceptor));
}
public RestfulServerExtension withDefaultResponseEncoding(EncodingEnum theEncodingEnum) {
return withServer(t -> t.setDefaultResponseEncoding(theEncodingEnum));
}
}