Fix #374 - Include ETag and Last-Modified headers in response

This commit is contained in:
James Agnew 2016-05-31 13:44:21 -04:00
parent a2954ef181
commit 39a96f0258
11 changed files with 376 additions and 179 deletions

View File

@ -24,6 +24,7 @@ import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
@ -45,9 +46,11 @@ import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.PreferReturnEnum;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.SummaryEnum;
import ca.uhn.fhir.rest.client.BaseHttpClientInvocation;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.EncodingEnum;
import ca.uhn.fhir.rest.server.IRestfulResponse;
import ca.uhn.fhir.rest.server.IRestfulServer;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
@ -58,6 +61,8 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding<MethodOutcome> {
static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseOutcomeReturningMethodBinding.class);
private static EnumSet<RestOperationTypeEnum> ourOperationsWhichAllowPreferHeader = EnumSet.of(RestOperationTypeEnum.CREATE, RestOperationTypeEnum.UPDATE);
private boolean myReturnVoid;
public BaseOutcomeReturningMethodBinding(Method theMethod, FhirContext theContext, Class<?> theMethodAnnotation, Object theProvider) {
@ -89,6 +94,43 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding<Metho
*/
protected abstract String getMatchingOperation();
private int getOperationStatus(MethodOutcome response) {
switch (getRestOperationType()) {
case CREATE:
if (response == null) {
throw new InternalErrorException("Method " + getMethod().getName() + " in type " + getMethod().getDeclaringClass().getCanonicalName() + " returned null, which is not allowed for create operation");
}
if (response.getCreated() == null || Boolean.TRUE.equals(response.getCreated())) {
return Constants.STATUS_HTTP_201_CREATED;
} else {
return Constants.STATUS_HTTP_200_OK;
}
case UPDATE:
if (response == null || response.getCreated() == null || Boolean.FALSE.equals(response.getCreated())) {
return Constants.STATUS_HTTP_200_OK;
} else {
return Constants.STATUS_HTTP_201_CREATED;
}
case VALIDATE:
case DELETE:
default:
if (response == null) {
if (isReturnVoid() == false) {
throw new InternalErrorException("Method " + getMethod().getName() + " in type " + getMethod().getDeclaringClass().getCanonicalName() + " returned null");
}
return Constants.STATUS_HTTP_204_NO_CONTENT;
} else {
if (response.getOperationOutcome() == null) {
return Constants.STATUS_HTTP_204_NO_CONTENT;
} else {
return Constants.STATUS_HTTP_200_OK;
}
}
}
}
@Override
public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) {
Set<RequestTypeEnum> allowableRequestTypes = provideAllowableRequestTypes();
@ -156,50 +198,18 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding<Metho
return returnResponse(theServer, theRequest, response, outcome, resource);
}
private int getOperationStatus(MethodOutcome response) {
switch (getRestOperationType()) {
case CREATE:
if (response == null) {
throw new InternalErrorException("Method " + getMethod().getName() + " in type " + getMethod().getDeclaringClass().getCanonicalName() + " returned null, which is not allowed for create operation");
}
if (response.getCreated() == null || Boolean.TRUE.equals(response.getCreated())) {
return Constants.STATUS_HTTP_201_CREATED;
} else {
return Constants.STATUS_HTTP_200_OK;
}
case UPDATE:
if (response == null || response.getCreated() == null || Boolean.FALSE.equals(response.getCreated())) {
return Constants.STATUS_HTTP_200_OK;
} else {
return Constants.STATUS_HTTP_201_CREATED;
}
case VALIDATE:
case DELETE:
default:
if (response == null) {
if (isReturnVoid() == false) {
throw new InternalErrorException("Method " + getMethod().getName() + " in type " + getMethod().getDeclaringClass().getCanonicalName() + " returned null");
}
return Constants.STATUS_HTTP_204_NO_CONTENT;
} else {
if (response.getOperationOutcome() == null) {
return Constants.STATUS_HTTP_204_NO_CONTENT;
} else {
return Constants.STATUS_HTTP_200_OK;
}
}
}
public boolean isReturnVoid() {
return myReturnVoid;
}
protected abstract Set<RequestTypeEnum> provideAllowableRequestTypes();
private Object returnResponse(IRestfulServer<?> theServer, RequestDetails theRequest, MethodOutcome response, IBaseResource originalOutcome, IBaseResource resource) throws IOException {
boolean allowPrefer = false;
int operationStatus = getOperationStatus(response);
IBaseResource outcome = originalOutcome;
if (EnumSet.of(RestOperationTypeEnum.CREATE, RestOperationTypeEnum.UPDATE).contains(getRestOperationType())) {
if (ourOperationsWhichAllowPreferHeader.contains(getRestOperationType())) {
allowPrefer = true;
}
@ -221,14 +231,31 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding<Metho
}
}
return theRequest.getResponse().returnResponse(ParseAction.create(outcome), operationStatus, allowPrefer, response, getResourceName());
}
IRestfulResponse restfulResponse = theRequest.getResponse();
public boolean isReturnVoid() {
return myReturnVoid;
}
if (response != null) {
if (response.getResource() != null) {
restfulResponse.setOperationResourceLastUpdated(RestfulServerUtils.extractLastUpdatedFromResource(response.getResource()));
}
protected abstract Set<RequestTypeEnum> provideAllowableRequestTypes();
IIdType responseId = response.getId();
if (responseId != null && responseId.getResourceType() == null && responseId.hasIdPart()) {
responseId = responseId.withResourceType(getResourceName());
}
if (responseId != null) {
String serverBase = theRequest.getFhirServerBase();
responseId = RestfulServerUtils.fullyQualifyResourceIdOrReturnNull(theServer, resource, serverBase, responseId);
restfulResponse.setOperationResourceId(responseId);
}
}
boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theServer, theRequest);
Set<SummaryEnum> summaryMode = Collections.emptySet();
return restfulResponse.streamResponseAsResource(outcome, prettyPrint, summaryMode, operationStatus, theRequest.isRespondGzip(), true);
// return theRequest.getResponse().returnResponse(ParseAction.create(outcome), operationStatus, allowPrefer, response, getResourceName());
}
protected void streamOperationOutcome(BaseServerResponseException theE, RestfulServer theServer, EncodingEnum theEncodingNotNull, HttpServletResponse theResponse, RequestDetails theRequest) throws IOException {
theResponse.setStatus(theE.getStatusCode());

View File

@ -23,10 +23,13 @@ package ca.uhn.fhir.rest.server;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.Date;
import java.util.Set;
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 ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.rest.api.MethodOutcome;
@ -35,10 +38,14 @@ import ca.uhn.fhir.rest.method.ParseAction;
public interface IRestfulResponse {
Object streamResponseAsResource(IBaseResource resource, boolean prettyPrint, Set<SummaryEnum> summaryMode, int operationStatus, boolean respondGzip, boolean addContentLocationHeader) throws IOException;
Object streamResponseAsResource(IBaseResource theActualResourceToReturn, boolean prettyPrint, Set<SummaryEnum> summaryMode, int operationStatus, boolean respondGzip, boolean addContentLocationHeader) throws IOException;
Object streamResponseAsBundle(Bundle bundle, Set<SummaryEnum> summaryMode, boolean respondGzip, boolean requestIsBrowser) throws IOException;
/**
* This is only used for DSTU1 getTags operations, so it can be removed at some point when we
* drop DSTU1
*/
Object returnResponse(ParseAction<?> outcome, int operationStatus, boolean allowPrefer, MethodOutcome response, String resourceName) throws IOException;
Writer getResponseWriter(int statusCode, String contentType, String charset, boolean respondGzip) throws UnsupportedEncodingException, IOException;
@ -49,4 +56,8 @@ public interface IRestfulResponse {
Object sendAttachmentResponse(IBaseBinary bin, int stausCode, String contentType) throws IOException;
void setOperationResourceLastUpdated(IPrimitiveType<Date> theOperationResourceLastUpdated);
void setOperationResourceId(IIdType theOperationResourceId);
}

View File

@ -21,10 +21,13 @@ package ca.uhn.fhir.rest.server;
*/
import java.io.IOException;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.rest.api.SummaryEnum;
@ -32,27 +35,15 @@ import ca.uhn.fhir.rest.method.RequestDetails;
public abstract class RestfulResponse<T extends RequestDetails> implements IRestfulResponse {
private T theRequestDetails;
private IIdType myOperationResourceId;
private IPrimitiveType<Date> myOperationResourceLastUpdated;
private ConcurrentHashMap<String, String> theHeaders = new ConcurrentHashMap<String, String>();
private T theRequestDetails;
public RestfulResponse(T requestDetails) {
this.theRequestDetails = requestDetails;
}
@Override
public final Object streamResponseAsResource(IBaseResource resource, boolean prettyPrint, Set<SummaryEnum> summaryMode,
int statusCode, boolean respondGzip, boolean addContentLocationHeader)
throws IOException {
return RestfulServerUtils.streamResponseAsResource(theRequestDetails.getServer(), resource, summaryMode, statusCode, addContentLocationHeader, respondGzip, getRequestDetails());
}
@Override
public Object streamResponseAsBundle(Bundle bundle, Set<SummaryEnum> summaryMode, boolean respondGzip, boolean requestIsBrowser)
throws IOException {
return RestfulServerUtils.streamResponseAsBundle(theRequestDetails.getServer(), bundle, summaryMode, respondGzip, getRequestDetails());
}
@Override
public void addHeader(String headerKey, String headerValue) {
this.getHeaders().put(headerKey, headerValue);
@ -74,6 +65,16 @@ public abstract class RestfulResponse<T extends RequestDetails> implements IRest
return theRequestDetails;
}
@Override
public void setOperationResourceId(IIdType theOperationResourceId) {
myOperationResourceId = theOperationResourceId;
}
@Override
public void setOperationResourceLastUpdated(IPrimitiveType<Date> theOperationResourceLastUpdated) {
myOperationResourceLastUpdated = theOperationResourceLastUpdated;
}
/**
* Set the requestDetails
* @param requestDetails the requestDetails to set
@ -82,4 +83,18 @@ public abstract class RestfulResponse<T extends RequestDetails> implements IRest
this.theRequestDetails = requestDetails;
}
@Override
public Object streamResponseAsBundle(Bundle bundle, Set<SummaryEnum> summaryMode, boolean respondGzip, boolean requestIsBrowser)
throws IOException {
return RestfulServerUtils.streamResponseAsBundle(theRequestDetails.getServer(), bundle, summaryMode, respondGzip, getRequestDetails());
}
@Override
public final Object streamResponseAsResource(IBaseResource resource, boolean prettyPrint, Set<SummaryEnum> summaryMode,
int statusCode, boolean respondGzip, boolean addContentLocationHeader)
throws IOException {
return RestfulServerUtils.streamResponseAsResource(theRequestDetails.getServer(), resource, summaryMode, statusCode, addContentLocationHeader, respondGzip, getRequestDetails(), myOperationResourceId, myOperationResourceLastUpdated);
}
}

View File

@ -46,8 +46,10 @@ import org.hl7.fhir.instance.model.api.IAnyResource;
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 ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.Include;
@ -113,8 +115,7 @@ public class RestfulServerUtils {
}
}
public static String createPagingLink(Set<Include> theIncludes, String theServerBase, String theSearchId, int theOffset, int theCount, EncodingEnum theResponseEncoding, boolean thePrettyPrint,
BundleTypeEnum theBundleType) {
public static String createPagingLink(Set<Include> theIncludes, String theServerBase, String theSearchId, int theOffset, int theCount, EncodingEnum theResponseEncoding, boolean thePrettyPrint, BundleTypeEnum theBundleType) {
try {
StringBuilder b = new StringBuilder();
b.append(theServerBase);
@ -204,7 +205,8 @@ public class RestfulServerUtils {
}
/**
* Returns null if the request doesn't express that it wants FHIR. If it expresses that it wants XML and JSON equally, returns thePrefer.
* Returns null if the request doesn't express that it wants FHIR. If it expresses that it wants XML and JSON
* equally, returns thePrefer.
*/
public static EncodingEnum determineResponseEncodingNoDefault(RequestDetails theReq, EncodingEnum thePrefer) {
String[] format = theReq.getParameters().get(Constants.PARAM_FORMAT);
@ -240,7 +242,7 @@ public class RestfulServerUtils {
float bestQ = -1f;
EncodingEnum retVal = null;
if (acceptValues != null) {
for (String nextAcceptHeaderValue : acceptValues) {
for (String nextAcceptHeaderValue : acceptValues) {
StringTokenizer tok = new StringTokenizer(nextAcceptHeaderValue, ",");
while (tok.hasMoreTokens()) {
String nextToken = tok.nextToken();
@ -348,7 +350,8 @@ public class RestfulServerUtils {
}
/**
* Determine whether a response should be given in JSON or XML format based on the incoming HttpServletRequest's <code>"_format"</code> parameter and <code>"Accept:"</code> HTTP header.
* Determine whether a response should be given in JSON or XML format based on the incoming HttpServletRequest's
* <code>"_format"</code> parameter and <code>"Accept:"</code> HTTP header.
*/
public static EncodingEnum determineResponseEncodingWithDefault(RequestDetails theReq) {
EncodingEnum retVal = determineResponseEncodingNoDefault(theReq, theReq.getServer().getDefaultResponseEncoding());
@ -365,7 +368,8 @@ public class RestfulServerUtils {
if (retVal == null) {
/*
* HAPI originally supported a custom parameter called _narrative, but this has been superceded by an official parameter called _summary
* HAPI originally supported a custom parameter called _narrative, but this has been superceded by an official
* parameter called _summary
*/
String[] narrative = requestParams.get(Constants.PARAM_NARRATIVE);
if (narrative != null && narrative.length > 0) {
@ -439,8 +443,7 @@ public class RestfulServerUtils {
try {
q = Float.parseFloat(value);
q = Math.max(q, 0.0f);
}
catch (NumberFormatException e) {
} catch (NumberFormatException e) {
ourLog.debug("Invalid Accept header q value: {}", value);
}
}
@ -524,8 +527,7 @@ public class RestfulServerUtils {
return prettyPrint;
}
public static Object streamResponseAsBundle(IRestfulServerDefaults theServer, Bundle bundle, Set<SummaryEnum> theSummaryMode, boolean respondGzip, RequestDetails theRequestDetails)
throws IOException {
public static Object streamResponseAsBundle(IRestfulServerDefaults theServer, Bundle bundle, Set<SummaryEnum> theSummaryMode, boolean respondGzip, RequestDetails theRequestDetails) throws IOException {
int status = 200;
@ -543,34 +545,42 @@ public class RestfulServerUtils {
parser.setEncodeElements(TEXT_ENCODE_ELEMENTS);
}
parser.encodeBundleToWriter(bundle, writer);
}
catch (Exception e) {
} catch (Exception e) {
//always send a response, even if the parsing went wrong
}
return theRequestDetails.getResponse().sendWriterResponse(status, contentType, charset, writer);
}
public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource, Set<SummaryEnum> theSummaryMode,
int stausCode, boolean theAddContentLocationHeader, boolean respondGzip,
RequestDetails theRequestDetails)
throws IOException {
public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource, Set<SummaryEnum> theSummaryMode, int stausCode, boolean theAddContentLocationHeader, boolean respondGzip, RequestDetails theRequestDetails) throws IOException {
return streamResponseAsResource(theServer, theResource, theSummaryMode, stausCode, theAddContentLocationHeader, respondGzip, theRequestDetails, null, null);
}
public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource, Set<SummaryEnum> theSummaryMode, int stausCode, boolean theAddContentLocationHeader, boolean respondGzip, RequestDetails theRequestDetails, IIdType theOperationResourceId, IPrimitiveType<Date> theOperationResourceLastUpdated) throws IOException {
IRestfulResponse restUtil = theRequestDetails.getResponse();
// Determine response encoding
EncodingEnum responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequestDetails,
theServer.getDefaultResponseEncoding());
EncodingEnum responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequestDetails, theServer.getDefaultResponseEncoding());
String serverBase = theRequestDetails.getFhirServerBase();
if (theAddContentLocationHeader && theResource.getIdElement() != null && theResource.getIdElement().hasIdPart()
&& isNotBlank(serverBase)) {
String resName = theServer.getFhirContext().getResourceDefinition(theResource).getName();
IIdType fullId = theResource.getIdElement().withServerBase(serverBase, resName);
IIdType fullId = null;
if (theOperationResourceId != null) {
fullId = theOperationResourceId;
} else if (theResource != null) {
if (theResource.getIdElement() != null) {
IIdType resourceId = theResource.getIdElement();
fullId = fullyQualifyResourceIdOrReturnNull(theServer, theResource, serverBase, resourceId);
}
}
if (theAddContentLocationHeader && fullId != null) {
restUtil.addHeader(Constants.HEADER_LOCATION, fullId.getValue());
restUtil.addHeader(Constants.HEADER_CONTENT_LOCATION, fullId.getValue());
}
if (theServer.getETagSupport() == ETagSupportEnum.ENABLED) {
if (theResource.getIdElement().hasVersionIdPart()) {
restUtil.addHeader(Constants.HEADER_ETAG, "W/\"" + theResource.getIdElement().getVersionIdPart() + '"');
if (fullId != null && fullId.hasVersionIdPart()) {
restUtil.addHeader(Constants.HEADER_ETAG, "W/\"" + fullId.getVersionIdPart() + '"');
}
}
@ -595,7 +605,8 @@ public class RestfulServerUtils {
boolean encodingDomainResourceAsText = theSummaryMode.contains(SummaryEnum.TEXT);
if (encodingDomainResourceAsText) {
/*
* If the user requests "text" for a bundle, only suppress the non text elements in the Element.entry.resource parts, we're not streaming just the narrative as HTML (since bundles don't even
* If the user requests "text" for a bundle, only suppress the non text elements in the Element.entry.resource
* parts, we're not streaming just the narrative as HTML (since bundles don't even
* have one)
*/
if ("Bundle".equals(theServer.getFhirContext().getResourceDefinition(theResource).getName())) {
@ -603,19 +614,25 @@ public class RestfulServerUtils {
}
}
if (encodingDomainResourceAsText) {
contentType = Constants.CT_HTML;
/*
* Last-Modified header
*/
IPrimitiveType<Date> lastUpdated;
if (theOperationResourceLastUpdated != null) {
lastUpdated = theOperationResourceLastUpdated;
} else {
contentType = responseEncoding.getResourceContentType();
lastUpdated = extractLastUpdatedFromResource(theResource);
}
if (lastUpdated != null && lastUpdated.isEmpty() == false) {
restUtil.addHeader(Constants.HEADER_LAST_MODIFIED, DateUtils.formatDate(lastUpdated.getValue()));
}
String charset = Constants.CHARSET_NAME_UTF8;
if (theResource instanceof IResource) {
InstantDt lastUpdated = ResourceMetadataKeyEnum.UPDATED.get((IResource) theResource);
if (lastUpdated != null && lastUpdated.isEmpty() == false) {
restUtil.addHeader(Constants.HEADER_LAST_MODIFIED, DateUtils.formatDate(lastUpdated.getValue()));
}
/*
* Category header (DSTU1 only)
*/
if (theResource instanceof IResource && theServer.getFhirContext().getVersion().getVersion() == FhirVersionEnum.DSTU1) {
TagList list = (TagList) ((IResource) theResource).getResourceMetadata().get(ResourceMetadataKeyEnum.TAG_LIST);
if (list != null) {
for (Tag tag : list) {
@ -624,16 +641,25 @@ public class RestfulServerUtils {
}
}
}
} else {
Date lastUpdated = ((IAnyResource) theResource).getMeta().getLastUpdated();
if (lastUpdated != null) {
restUtil.addHeader(Constants.HEADER_LAST_MODIFIED, DateUtils.formatDate(lastUpdated));
}
}
Writer writer = restUtil.getResponseWriter(stausCode, contentType, charset, respondGzip);
/*
* Stream the response body
*/
if (encodingDomainResourceAsText && theResource instanceof IResource) {
if (theResource == null) {
contentType = null;
} else if (encodingDomainResourceAsText) {
contentType = Constants.CT_HTML;
} else {
contentType = responseEncoding.getResourceContentType();
}
String charset = Constants.CHARSET_NAME_UTF8;
Writer writer = restUtil.getResponseWriter(stausCode, contentType, charset, respondGzip);
if (theResource == null) {
// No response is being returned
} else if (encodingDomainResourceAsText && theResource instanceof IResource) {
writer.append(((IResource) theResource).getText().getDiv().getValueAsString());
} else {
IParser parser = getNewParser(theServer.getFhirContext(), theRequestDetails);
@ -643,6 +669,30 @@ public class RestfulServerUtils {
return restUtil.sendWriterResponse(stausCode, contentType, charset, writer);
}
public static IIdType fullyQualifyResourceIdOrReturnNull(IRestfulServerDefaults theServer, IBaseResource theResource, String theServerBase, IIdType theResourceId) {
IIdType retVal = null;
if (theResourceId.hasIdPart() && isNotBlank(theServerBase)) {
String resName = theResourceId.getResourceType();
if (theResource != null && isBlank(resName)) {
resName = theServer.getFhirContext().getResourceDefinition(theResource).getName();
}
if (isNotBlank(resName)) {
retVal = theResourceId.withServerBase(theServerBase, resName);
}
}
return retVal;
}
public static IPrimitiveType<Date> extractLastUpdatedFromResource(IBaseResource theResource) {
IPrimitiveType<Date> lastUpdated = null;
if (theResource instanceof IResource) {
lastUpdated = ResourceMetadataKeyEnum.UPDATED.get((IResource) theResource);
} else if (theResource instanceof IAnyResource) {
lastUpdated = new InstantDt(((IAnyResource) theResource).getMeta().getLastUpdated());
}
return lastUpdated;
}
// static Integer tryToExtractNamedParameter(HttpServletRequest theRequest, String name) {
// String countString = theRequest.getParameter(name);
// Integer count = null;

View File

@ -1,5 +1,7 @@
package ca.uhn.fhir.jaxrs.server.util;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/*
* #%L
* HAPI FHIR JAX-RS Server
@ -69,11 +71,15 @@ public class JaxRsResponse extends RestfulResponse<JaxRsRequest> {
}
@Override
public Response sendWriterResponse(int status, String contentType, String charset, Writer writer) {
String charContentType = contentType + "; charset="
+ StringUtils.defaultIfBlank(charset, Constants.CHARSET_NAME_UTF8);
return buildResponse(status).header(Constants.HEADER_CONTENT_TYPE, charContentType).entity(writer.toString())
.build();
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);
builder.header(Constants.HEADER_CONTENT_TYPE, charContentType);
}
builder.entity(theWriter.toString());
Response retVal = builder.build();
return retVal;
}
@Override

View File

@ -47,9 +47,6 @@ import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
/**
* Created by dsotnikov on 2/25/2014.
*/
public class CreateTest {
private static CloseableHttpClient ourClient;
private static final FhirContext ourCtx = FhirContext.forDstu1();
@ -85,7 +82,7 @@ public class CreateTest {
assertEquals(201, status.getStatusLine().getStatusCode());
assertEquals("http://localhost:" + ourPort + "/Patient/001/_history/002", status.getFirstHeader("location").getValue());
assertThat(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue().toUpperCase(), StringContains.containsString("UTF-8"));
assertNull(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE));
assertThat(ourLastResourceBody, stringContainsInOrder("<Patient ", "<identifier>","<value value=\"001"));
assertEquals(EncodingEnum.XML, ourLastEncoding);
@ -176,7 +173,7 @@ public class CreateTest {
assertEquals(201, status.getStatusLine().getStatusCode());
assertEquals("http://localhost:" + ourPort + "/Organization/001", status.getFirstHeader("location").getValue());
assertThat(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue(), StringContains.containsStringIgnoringCase("utf-8"));
assertNull(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE));
assertThat(ourLastResourceBody, stringContainsInOrder("\"resourceType\":\"Organization\"", "\"identifier\"","\"value\":\"001"));
assertEquals(EncodingEnum.JSON, ourLastEncoding);

View File

@ -49,7 +49,7 @@ public class DeleteDstu2Test {
@Test
public void testUpdateWithConditionalUrl() throws Exception {
public void testDeleteWithConditionalUrl() throws Exception {
Patient patient = new Patient();
patient.addIdentifier().setValue("002");
@ -64,7 +64,7 @@ public class DeleteDstu2Test {
}
@Test
public void testUpdateWithoutConditionalUrl() throws Exception {
public void testDeleteWithoutConditionalUrl() throws Exception {
Patient patient = new Patient();
patient.addIdentifier().setValue("002");
@ -73,6 +73,7 @@ public class DeleteDstu2Test {
HttpResponse status = ourClient.execute(httpPost);
assertEquals(204, status.getStatusLine().getStatusCode());
assertNull(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE));
assertEquals("Patient/2", ourLastIdParam.toUnqualified().getValue());
assertNull(ourLastConditionalUrl);

View File

@ -3,9 +3,8 @@ package ca.uhn.fhir.rest.server;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.emptyOrNullString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.isEmptyOrNullString;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import java.util.concurrent.TimeUnit;
@ -21,12 +20,15 @@ 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.IBaseOperationOutcome;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu2.resource.OperationOutcome;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.annotation.Create;
@ -45,9 +47,15 @@ public class PreferTest {
private static int ourPort;
private static Server ourServer;
public static IBaseOperationOutcome ourReturnOperationOutcome;
@Before
public void before() {
ourReturnOperationOutcome = null;
}
@Test
public void testCreatePreferMinimal() throws Exception {
public void testCreatePreferMinimalNoOperationOutcome() throws Exception {
Patient patient = new Patient();
patient.addIdentifier().setValue("002");
@ -65,12 +73,41 @@ public class PreferTest {
assertEquals(Constants.STATUS_HTTP_201_CREATED, status.getStatusLine().getStatusCode());
assertThat(responseContent, is(emptyOrNullString()));
assertThat(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue(), not(containsString("fhir")));
// assertThat(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue(), not(containsString("fhir")));
assertNull(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE));
assertEquals("http://localhost:" + ourPort + "/Patient/001/_history/002", status.getFirstHeader("location").getValue());
assertEquals("http://localhost:" + ourPort + "/Patient/001/_history/002", status.getFirstHeader("content-location").getValue());
}
@Test
public void testCreatePreferMinimalWithOperationOutcome() throws Exception {
OperationOutcome oo = new OperationOutcome();
oo.addIssue().setDiagnostics("DIAG");
ourReturnOperationOutcome = oo;
Patient patient = new Patient();
patient.addIdentifier().setValue("002");
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient");
httpPost.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + "=" + Constants.HEADER_PREFER_RETURN_MINIMAL);
httpPost.setEntity(new StringEntity(ourCtx.newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
HttpResponse status = ourClient.execute(httpPost);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(Constants.STATUS_HTTP_201_CREATED, status.getStatusLine().getStatusCode());
assertThat(responseContent, containsString("DIAG"));
assertEquals("application/xml+fhir;charset=utf-8", status.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue().toLowerCase().replace(" ", ""));
assertEquals("http://localhost:" + ourPort + "/Patient/001/_history/002", status.getFirstHeader("location").getValue());
assertEquals("http://localhost:" + ourPort + "/Patient/001/_history/002", status.getFirstHeader("content-location").getValue());
}
@Test
public void testCreatePreferRepresentation() throws Exception {
@ -97,7 +134,6 @@ public class PreferTest {
}
@Test
public void testCreateWithNoPrefer() throws Exception {
@ -120,14 +156,12 @@ public class PreferTest {
}
@AfterClass
public static void afterClassClearContext() throws Exception {
ourServer.stop();
TestUtil.clearAllStaticFieldsForUnitTest();
}
@BeforeClass
public static void beforeClass() throws Exception {
ourPort = PortUtil.findFreePort();
@ -161,6 +195,8 @@ public class PreferTest {
pt.setId(id);
retVal.setResource(pt);
retVal.setOperationOutcome(ourReturnOperationOutcome);
return retVal;
}

View File

@ -1,7 +1,9 @@
package ca.uhn.fhir.rest.server;
import static org.hamcrest.Matchers.blankOrNullString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
@ -27,8 +29,10 @@ import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
import ca.uhn.fhir.rest.annotation.IdParam;
@ -40,19 +44,17 @@ import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
/**
* Created by dsotnikov on 2/25/2014.
*/
public class UpdateConditionalTest {
public class UpdateDstu2Test {
private static CloseableHttpClient ourClient;
private static FhirContext ourCtx = FhirContext.forDstu2();
private static String ourLastConditionalUrl;
private static IdDt ourLastId;
private static IdDt ourLastIdParam;
private static boolean ourLastRequestWasSearch;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(UpdateConditionalTest.class);
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(UpdateDstu2Test.class);
private static int ourPort;
private static Server ourServer;
private static InstantDt ourSetLastUpdated;
@Before
@ -113,6 +115,34 @@ public class UpdateConditionalTest {
}
@Test
public void testUpdateReturnsETagAndUpdate() throws Exception {
Patient patient = new Patient();
patient.setId("123");
patient.addIdentifier().setValue("002");
ourSetLastUpdated = new InstantDt("2002-04-22T11:22:33.022Z");
HttpPut httpPost = new HttpPut("http://localhost:" + ourPort + "/Patient/123");
httpPost.setEntity(new StringEntity(ourCtx.newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
HttpResponse status = ourClient.execute(httpPost);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
ourLog.info("Response was:\n{}", status);
assertThat(responseContent, blankOrNullString());
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals("http://localhost:" + ourPort + "/Patient/001/_history/002", status.getFirstHeader("location").getValue());
assertEquals("http://localhost:" + ourPort + "/Patient/001/_history/002", status.getFirstHeader("content-location").getValue());
assertEquals("W/\"002\"", status.getFirstHeader(Constants.HEADER_ETAG_LC).getValue());
assertEquals("Mon, 22 Apr 2002 11:22:33 GMT", status.getFirstHeader(Constants.HEADER_LAST_MODIFIED_LOWERCASE).getValue());
}
@Test
public void testUpdateWithoutConditionalUrl() throws Exception {
@ -188,7 +218,12 @@ public class UpdateConditionalTest {
ourLastConditionalUrl = theConditional;
ourLastId = thePatient.getId();
ourLastIdParam = theIdParam;
return new MethodOutcome(new IdDt("Patient/001/_history/002"));
MethodOutcome retVal = new MethodOutcome(new IdDt("Patient/001/_history/002"));
ResourceMetadataKeyEnum.UPDATED.put(thePatient, ourSetLastUpdated);
retVal.setResource(thePatient);
return retVal;
}
}

View File

@ -6,6 +6,8 @@ import static org.junit.Assert.assertNull;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.StringEntity;
@ -40,7 +42,6 @@ public class CreateBinaryDstu3Test {
private static int ourPort;
private static Server ourServer;
@Before
public void before() {
ourLastBinary = null;
@ -48,40 +49,42 @@ public class CreateBinaryDstu3Test {
ourLastBinaryString = null;
}
@Test
public void testRawBytesBinaryContentType() throws Exception {
HttpPost post = new HttpPost("http://localhost:" + ourPort + "/Binary");
post.setEntity(new ByteArrayEntity(new byte[] {0,1,2,3,4}));
post.setEntity(new ByteArrayEntity(new byte[] { 0, 1, 2, 3, 4 }));
post.addHeader("Content-Type", "application/foo");
ourClient.execute(post);
assertEquals("application/foo", ourLastBinary.getContentType());
assertArrayEquals(new byte[] {0,1,2,3,4}, ourLastBinary.getContent());
assertArrayEquals(new byte[] {0,1,2,3,4}, ourLastBinaryBytes);
CloseableHttpResponse status = ourClient.execute(post);
try {
assertEquals("application/foo", ourLastBinary.getContentType());
assertArrayEquals(new byte[] { 0, 1, 2, 3, 4 }, ourLastBinary.getContent());
assertArrayEquals(new byte[] { 0, 1, 2, 3, 4 }, ourLastBinaryBytes);
} finally {
IOUtils.closeQuietly(status);
}
}
/**
* Technically the client shouldn't be doing it this way,
* but we'll be accepting
* Technically the client shouldn't be doing it this way, but we'll be accepting
*/
@Test
public void testRawBytesFhirContentType() throws Exception {
Binary b = new Binary();
b.setContentType("application/foo");
b.setContent(new byte[] {0,1,2,3,4});
b.setContent(new byte[] { 0, 1, 2, 3, 4 });
String encoded = ourCtx.newJsonParser().encodeResourceToString(b);
HttpPost post = new HttpPost("http://localhost:" + ourPort + "/Binary");
post.setEntity(new StringEntity(encoded));
post.addHeader("Content-Type", Constants.CT_FHIR_JSON);
ourClient.execute(post);
assertEquals("application/foo", ourLastBinary.getContentType());
assertArrayEquals(new byte[] {0,1,2,3,4}, ourLastBinary.getContent());
CloseableHttpResponse status = ourClient.execute(post);
try {
assertEquals("application/foo", ourLastBinary.getContentType());
assertArrayEquals(new byte[] { 0, 1, 2, 3, 4 }, ourLastBinary.getContent());
} finally {
IOUtils.closeQuietly(status);
}
}
@Test
@ -98,22 +101,28 @@ public class CreateBinaryDstu3Test {
HttpPost post = new HttpPost("http://localhost:" + ourPort + "/Binary");
post.setEntity(new StringEntity(encoded));
post.addHeader("Content-Type", Constants.CT_FHIR_JSON);
ourClient.execute(post);
assertEquals("application/xml+fhir", ourLastBinary.getContentType());
assertArrayEquals(b.getContent(), ourLastBinary.getContent());
assertEquals(encoded, ourLastBinaryString);
assertArrayEquals(encoded.getBytes("UTF-8"), ourLastBinaryBytes);
CloseableHttpResponse status = ourClient.execute(post);
try {
assertEquals("application/xml+fhir", ourLastBinary.getContentType());
assertArrayEquals(b.getContent(), ourLastBinary.getContent());
assertEquals(encoded, ourLastBinaryString);
assertArrayEquals(encoded.getBytes("UTF-8"), ourLastBinaryBytes);
} finally {
IOUtils.closeQuietly(status);
}
}
@Test
public void testRawBytesNoContentType() throws Exception {
HttpPost post = new HttpPost("http://localhost:" + ourPort + "/Binary");
post.setEntity(new ByteArrayEntity(new byte[] {0,1,2,3,4}));
ourClient.execute(post);
assertNull(ourLastBinary.getContentType());
assertArrayEquals(new byte[] {0,1,2,3,4}, ourLastBinary.getContent());
post.setEntity(new ByteArrayEntity(new byte[] { 0, 1, 2, 3, 4 }));
CloseableHttpResponse status = ourClient.execute(post);
try {
assertNull(ourLastBinary.getContentType());
assertArrayEquals(new byte[] { 0, 1, 2, 3, 4 }, ourLastBinary.getContent());
} finally {
IOUtils.closeQuietly(status);
}
}
@AfterClass
@ -122,7 +131,6 @@ public class CreateBinaryDstu3Test {
TestUtil.clearAllStaticFieldsForUnitTest();
}
@BeforeClass
public static void beforeClass() throws Exception {
ourPort = PortUtil.findFreePort();

View File

@ -248,6 +248,17 @@
Improve error messages when the $validate operation is called but no resource
is actually supplied to validate
</action>
<action type="remove">
DSTU2+ servers no longer return the Category header, as this has been
removed from the FHIR specification (and tags are now available in the
resource body so the header was duplication/wasted bandwidth)
</action>
<action type="fix" issue="374">
Create and Update operations in server did not
include ETag or Last-Modified headers even though
the spec says they should. Thanks to Jim Steel for
reporting!
</action>
</release>
<release version="1.5" date="2016-04-20">
<action type="fix" issue="339">