diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java index 55aa1aabb11..25cb667f936 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java @@ -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 { static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseOutcomeReturningMethodBinding.class); + private static EnumSet 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 allowableRequestTypes = provideAllowableRequestTypes(); @@ -156,50 +198,18 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding 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; } @@ -220,16 +230,33 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding summaryMode = Collections.emptySet(); + + return restfulResponse.streamResponseAsResource(outcome, prettyPrint, summaryMode, operationStatus, theRequest.isRespondGzip(), true); +// return theRequest.getResponse().returnResponse(ParseAction.create(outcome), operationStatus, allowPrefer, response, getResourceName()); } - public boolean isReturnVoid() { - return myReturnVoid; - } - - protected abstract Set provideAllowableRequestTypes(); - protected void streamOperationOutcome(BaseServerResponseException theE, RestfulServer theServer, EncodingEnum theEncodingNotNull, HttpServletResponse theResponse, RequestDetails theRequest) throws IOException { theResponse.setStatus(theE.getStatusCode()); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IRestfulResponse.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IRestfulResponse.java index 3b8fdb5c50b..fe0509e9f3e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IRestfulResponse.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IRestfulResponse.java @@ -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 summaryMode, int operationStatus, boolean respondGzip, boolean addContentLocationHeader) throws IOException; + Object streamResponseAsResource(IBaseResource theActualResourceToReturn, boolean prettyPrint, Set summaryMode, int operationStatus, boolean respondGzip, boolean addContentLocationHeader) throws IOException; Object streamResponseAsBundle(Bundle bundle, Set 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 theOperationResourceLastUpdated); + + void setOperationResourceId(IIdType theOperationResourceId); + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulResponse.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulResponse.java index f7e2ff4d22a..5fee3043aba 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulResponse.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulResponse.java @@ -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 implements IRestfulResponse { - private T theRequestDetails; + private IIdType myOperationResourceId; + private IPrimitiveType myOperationResourceLastUpdated; private ConcurrentHashMap theHeaders = new ConcurrentHashMap(); + private T theRequestDetails; public RestfulResponse(T requestDetails) { this.theRequestDetails = requestDetails; } - @Override - public final Object streamResponseAsResource(IBaseResource resource, boolean prettyPrint, Set 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 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 implements IRest return theRequestDetails; } + @Override + public void setOperationResourceId(IIdType theOperationResourceId) { + myOperationResourceId = theOperationResourceId; + } + + @Override + public void setOperationResourceLastUpdated(IPrimitiveType theOperationResourceLastUpdated) { + myOperationResourceLastUpdated = theOperationResourceLastUpdated; + } + /** * Set the requestDetails * @param requestDetails the requestDetails to set @@ -82,4 +83,18 @@ public abstract class RestfulResponse implements IRest this.theRequestDetails = requestDetails; } + @Override + public Object streamResponseAsBundle(Bundle bundle, Set 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 summaryMode, + int statusCode, boolean respondGzip, boolean addContentLocationHeader) + throws IOException { + return RestfulServerUtils.streamResponseAsResource(theRequestDetails.getServer(), resource, summaryMode, statusCode, addContentLocationHeader, respondGzip, getRequestDetails(), myOperationResourceId, myOperationResourceLastUpdated); + + } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java index 3089e68f60e..fa911faa3bc 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java @@ -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 theIncludes, String theServerBase, String theSearchId, int theOffset, int theCount, EncodingEnum theResponseEncoding, boolean thePrettyPrint, - BundleTypeEnum theBundleType) { + public static String createPagingLink(Set 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); @@ -230,7 +232,7 @@ public class RestfulServerUtils { if ("Binary".equals(theReq.getResourceName())) { contentTypeToEncoding = EncodingEnum.getContentTypeToEncodingStrict(); } - + /* * The Accept header is kind of ridiculous, e.g. */ @@ -239,8 +241,8 @@ public class RestfulServerUtils { List acceptValues = theReq.getHeaders(Constants.HEADER_ACCEPT); float bestQ = -1f; EncodingEnum retVal = null; - if (acceptValues != null) { - for (String nextAcceptHeaderValue : acceptValues) { + if (acceptValues != null) { + 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 "_format" parameter and "Accept:" HTTP header. + * Determine whether a response should be given in JSON or XML format based on the incoming HttpServletRequest's + * "_format" parameter and "Accept:" 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 theSummaryMode, boolean respondGzip, RequestDetails theRequestDetails) - throws IOException { + public static Object streamResponseAsBundle(IRestfulServerDefaults theServer, Bundle bundle, Set 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 theSummaryMode, - int stausCode, boolean theAddContentLocationHeader, boolean respondGzip, - RequestDetails theRequestDetails) - throws IOException { + public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource, Set 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 theSummaryMode, int stausCode, boolean theAddContentLocationHeader, boolean respondGzip, RequestDetails theRequestDetails, IIdType theOperationResourceId, IPrimitiveType 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 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,25 +641,58 @@ public class RestfulServerUtils { } } } - } else { - Date lastUpdated = ((IAnyResource) theResource).getMeta().getLastUpdated(); - if (lastUpdated != null) { - restUtil.addHeader(Constants.HEADER_LAST_MODIFIED, DateUtils.formatDate(lastUpdated)); - } } + + /* + * Stream the response body + */ + + 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 (encodingDomainResourceAsText && theResource instanceof IResource) { + 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); parser.encodeResourceToWriter(theResource, writer); } - + 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 extractLastUpdatedFromResource(IBaseResource theResource) { + IPrimitiveType 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; diff --git a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java index 68fc8bc7326..c6b0a729fb0 100644 --- a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java +++ b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java @@ -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 { } @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 diff --git a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/CreateTest.java b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/CreateTest.java index 30cc25fabab..82262d86361 100644 --- a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/CreateTest.java +++ b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/CreateTest.java @@ -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("","", responseContent); 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 testCreateWithNoPrefer() throws Exception { @@ -117,17 +153,15 @@ public class PreferTest { assertEquals(201, 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()); - + } - @AfterClass public static void afterClassClearContext() throws Exception { ourServer.stop(); TestUtil.clearAllStaticFieldsForUnitTest(); } - - + @BeforeClass public static void beforeClass() throws Exception { ourPort = PortUtil.findFreePort(); @@ -149,18 +183,20 @@ public class PreferTest { ourClient = builder.build(); } - + public static class PatientProvider implements IResourceProvider { @Create() public MethodOutcome createPatient(@ResourceParam Patient thePatient) { IdDt id = new IdDt("Patient/001/_history/002"); MethodOutcome retVal = new MethodOutcome(id); - + Patient pt = new Patient(); pt.setId(id); retVal.setResource(pt); - + + retVal.setOperationOutcome(ourReturnOperationOutcome); + return retVal; } @@ -173,11 +209,11 @@ public class PreferTest { public MethodOutcome updatePatient(@ResourceParam Patient thePatient, @IdParam IdDt theIdParam) { IdDt id = new IdDt("Patient/001/_history/002"); MethodOutcome retVal = new MethodOutcome(id); - + Patient pt = new Patient(); pt.setId(id); retVal.setResource(pt); - + return retVal; } diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/UpdateConditionalTest.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/UpdateDstu2Test.java similarity index 77% rename from hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/UpdateConditionalTest.java rename to hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/UpdateDstu2Test.java index 6c4860eae9d..f17ec7a8dbd 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/UpdateConditionalTest.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/UpdateDstu2Test.java @@ -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; } } diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/CreateBinaryDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/CreateBinaryDstu3Test.java index 236f65eb4a5..12fff2c67d6 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/CreateBinaryDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/CreateBinaryDstu3Test.java @@ -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; @@ -39,7 +41,6 @@ public class CreateBinaryDstu3Test { private static String ourLastBinaryString; private static int ourPort; private static Server ourServer; - @Before public void before() { @@ -48,72 +49,80 @@ 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 public void testRawBytesFhirContentTypeContainingFhir() throws Exception { - + Patient p = new Patient(); p.getText().setDivAsString("A PATIENT"); - + Binary b = new Binary(); b.setContentType("application/xml+fhir"); b.setContent(ourCtx.newXmlParser().encodeResourceToString(p).getBytes("UTF-8")); 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/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 @@ -121,8 +130,7 @@ public class CreateBinaryDstu3Test { ourServer.stop(); TestUtil.clearAllStaticFieldsForUnitTest(); } - - + @BeforeClass public static void beforeClass() throws Exception { ourPort = PortUtil.findFreePort(); @@ -143,7 +151,7 @@ public class CreateBinaryDstu3Test { builder.setConnectionManager(connectionManager); ourClient = builder.build(); } - + public static class BinaryProvider implements IResourceProvider { @Create() @@ -153,7 +161,7 @@ public class CreateBinaryDstu3Test { ourLastBinaryBytes = theBinaryBytes; return new MethodOutcome(new IdType("Binary/001/_history/002")); } - + @Override public Class getResourceType() { return Binary.class; diff --git a/src/changes/changes.xml b/src/changes/changes.xml index acd64b5fed4..7df1d9dc142 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -248,6 +248,17 @@ Improve error messages when the $validate operation is called but no resource is actually supplied to validate + + 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) + + + 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! +