diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ReadMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ReadMethodBinding.java index abe7fd75105..7b2aebb6381 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ReadMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ReadMethodBinding.java @@ -25,38 +25,25 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; -import org.hl7.fhir.instance.model.api.IBaseBinary; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.instance.model.api.*; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.model.api.Bundle; -import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.api.*; import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.model.valueset.BundleTypeEnum; -import ca.uhn.fhir.rest.annotation.Elements; -import ca.uhn.fhir.rest.annotation.IdParam; -import ca.uhn.fhir.rest.annotation.Read; +import ca.uhn.fhir.rest.annotation.*; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; -import ca.uhn.fhir.rest.server.Constants; -import ca.uhn.fhir.rest.server.ETagSupportEnum; -import ca.uhn.fhir.rest.server.IBundleProvider; -import ca.uhn.fhir.rest.server.IRestfulServer; -import ca.uhn.fhir.rest.server.SimpleBundleProvider; -import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.NotModifiedException; +import ca.uhn.fhir.rest.server.*; +import ca.uhn.fhir.rest.server.exceptions.*; +import ca.uhn.fhir.util.DateUtils; public class ReadMethodBinding extends BaseResourceReturningMethodBinding implements IClientResponseHandlerHandlesBinary { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ReadMethodBinding.class); @@ -220,22 +207,48 @@ public class ReadMethodBinding extends BaseResourceReturningMethodBinding implem Object response = invokeServerMethod(theServer, theRequest, theMethodParams); IBundleProvider retVal = toResourceList(response); - if (theRequest.getServer().getETagSupport() == ETagSupportEnum.ENABLED) { - String ifNoneMatch = theRequest.getHeader(Constants.HEADER_IF_NONE_MATCH_LC); - if (retVal.size() == 1 && StringUtils.isNotBlank(ifNoneMatch)) { - List responseResources = retVal.getResources(0, 1); - IBaseResource responseResource = responseResources.get(0); - ifNoneMatch = MethodUtil.parseETagValue(ifNoneMatch); - if (responseResource.getIdElement() != null && responseResource.getIdElement().hasVersionIdPart()) { - if (responseResource.getIdElement().getVersionIdPart().equals(ifNoneMatch)) { - ourLog.debug("Returning HTTP 301 because request specified {}={}", Constants.HEADER_IF_NONE_MATCH, ifNoneMatch); - throw new NotModifiedException("Not Modified"); + if (retVal.size() == 1) { + List responseResources = retVal.getResources(0, 1); + IBaseResource responseResource = responseResources.get(0); + + // If-None-Match + if (theRequest.getServer().getETagSupport() == ETagSupportEnum.ENABLED) { + String ifNoneMatch = theRequest.getHeader(Constants.HEADER_IF_NONE_MATCH_LC); + if (StringUtils.isNotBlank(ifNoneMatch)) { + ifNoneMatch = MethodUtil.parseETagValue(ifNoneMatch); + if (responseResource.getIdElement() != null && responseResource.getIdElement().hasVersionIdPart()) { + if (responseResource.getIdElement().getVersionIdPart().equals(ifNoneMatch)) { + ourLog.debug("Returning HTTP 301 because request specified {}={}", Constants.HEADER_IF_NONE_MATCH, ifNoneMatch); + throw new NotModifiedException("Not Modified"); + } } } } - } - + + // If-Modified-Since + String ifModifiedSince = theRequest.getHeader(Constants.HEADER_IF_MODIFIED_SINCE_LC); + if (isNotBlank(ifModifiedSince)) { + Date ifModifiedSinceDate = DateUtils.parseDate(ifModifiedSince); + Date lastModified = null; + if (responseResource instanceof IResource) { + InstantDt lastModifiedDt = ResourceMetadataKeyEnum.UPDATED.get((IResource) responseResource); + if (lastModifiedDt != null) { + lastModified = lastModifiedDt.getValue(); + } + } else { + lastModified = ((IAnyResource)responseResource).getMeta().getLastUpdated(); + } + + if (lastModified != null && lastModified.getTime() > ifModifiedSinceDate.getTime()) { + ourLog.debug("Returning HTTP 301 because If-Modified-Since does not match"); + throw new NotModifiedException("Not Modified"); + } + } + + } // if we have at least 1 result + + return retVal; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java index eaf860565a8..4785d4dd458 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java @@ -48,6 +48,7 @@ public class Constants { public static final String CT_XML = "application/xml"; public static final String CT_XML_PATCH = "application/xml-patch+xml"; public static final String ENCODING_GZIP = "gzip"; + public static final String EXTOP_PROCESS_MESSAGE = "$process-message"; //Used in messaging public static final String EXTOP_VALIDATE = "$validate"; public static final String EXTOP_VALIDATE_MODE = "mode"; public static final String EXTOP_VALIDATE_PROFILE = "profile"; @@ -86,6 +87,8 @@ public class Constants { public static final String HEADER_ETAG_LC = HEADER_ETAG.toLowerCase(); public static final String HEADER_IF_MATCH = "If-Match"; public static final String HEADER_IF_MATCH_LC = HEADER_IF_MATCH.toLowerCase(); + public static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since"; + public static final String HEADER_IF_MODIFIED_SINCE_LC = HEADER_IF_MODIFIED_SINCE.toLowerCase(); public static final String HEADER_IF_NONE_EXIST = "If-None-Exist"; public static final String HEADER_IF_NONE_EXIST_LC = HEADER_IF_NONE_EXIST.toLowerCase(); public static final String HEADER_IF_NONE_MATCH = "If-None-Match"; @@ -109,6 +112,7 @@ public class Constants { public static final String LINK_PREVIOUS = "previous"; public static final String LINK_SELF = "self"; public static final String OPENSEARCH_NS_OLDER = "http://purl.org/atompub/tombstones/1.0"; + public static final String PARAM_ASYNC = "async"; //Used in messaging public static final String PARAM_AT = "_at"; /** * Used in paging links @@ -133,6 +137,7 @@ public class Constants { public static final String PARAM_PRETTY_VALUE_TRUE = "true"; public static final String PARAM_PROFILE = "_profile"; public static final String PARAM_QUERY = "_query"; + public static final String PARAM_RESPONSE_URL = "response-url"; //Used in messaging public static final String PARAM_REVINCLUDE = "_revinclude"; public static final String PARAM_REVINCLUDE_RECURSE = PARAM_REVINCLUDE+PARAM_INCLUDE_QUALIFIER_RECURSE; public static final String PARAM_SEARCH = "_search"; @@ -142,13 +147,10 @@ public class Constants { public static final String PARAM_SORT_ASC = "_sort:asc"; public static final String PARAM_SORT_DESC = "_sort:desc"; public static final String PARAM_SUMMARY = "_summary"; - public static final String PARAM_TAG = "_tag"; - public static final String PARAM_TAGS = "_tags"; - public static final String PARAM_TEXT = "_text"; + public static final String PARAM_TAG = "_tag"; + public static final String PARAM_TAGS = "_tags"; + public static final String PARAM_TEXT = "_text"; public static final String PARAM_VALIDATE = "_validate"; - public static final String PARAM_ASYNC = "async"; //Used in messaging - public static final String PARAM_RESPONSE_URL = "response-url"; //Used in messaging - public static final String EXTOP_PROCESS_MESSAGE = "$process-message"; //Used in messaging public static final String PARAMQUALIFIER_MISSING = ":missing"; public static final String PARAMQUALIFIER_MISSING_FALSE = "false"; public static final String PARAMQUALIFIER_MISSING_TRUE = "true"; @@ -162,8 +164,8 @@ public class Constants { public static final int STATUS_HTTP_400_BAD_REQUEST = 400; public static final int STATUS_HTTP_401_CLIENT_UNAUTHORIZED = 401; public static final int STATUS_HTTP_403_FORBIDDEN = 403; - public static final int STATUS_HTTP_404_NOT_FOUND = 404; + public static final int STATUS_HTTP_404_NOT_FOUND = 404; public static final int STATUS_HTTP_405_METHOD_NOT_ALLOWED = 405; public static final int STATUS_HTTP_409_CONFLICT = 409; public static final int STATUS_HTTP_410_GONE = 410; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/DateUtils.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/DateUtils.java index ad811fa7f43..4cab7cf8c30 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/DateUtils.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/DateUtils.java @@ -61,8 +61,6 @@ import java.util.TimeZone; * A utility class for parsing and formatting HTTP dates as used in cookies and * other headers. This class handles dates as defined by RFC 2616 section * 3.3.1 as well as some other common non-standard formats. - * - * @since 4.3 */ public final class DateUtils { diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ReadDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ReadDstu2Test.java index 28b92861998..3462d547db9 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ReadDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ReadDstu2Test.java @@ -10,6 +10,7 @@ import java.util.concurrent.TimeUnit; import org.apache.commons.io.IOUtils; import org.apache.http.HttpResponse; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; @@ -17,10 +18,7 @@ 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.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; +import org.junit.*; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.IResource; @@ -28,13 +26,10 @@ import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.model.api.annotation.ResourceDef; 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.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Read; -import ca.uhn.fhir.rest.server.AddProfileTagEnum; -import ca.uhn.fhir.rest.server.IResourceProvider; -import ca.uhn.fhir.rest.server.RestfulServer; -import ca.uhn.fhir.util.PortUtil; -import ca.uhn.fhir.util.TestUtil; +import ca.uhn.fhir.util.*; public class ReadDstu2Test { @@ -54,23 +49,48 @@ public class ReadDstu2Test { ourLastId = null; } + @Test + public void testIfModifiedSince() throws Exception { + + CloseableHttpResponse status; + HttpGet httpGet; + + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2"); + httpGet.addHeader(Constants.HEADER_IF_MODIFIED_SINCE, DateUtils.formatDate(new InstantDt("2012-01-01T13:00:00Z").getValue())); + status = ourClient.execute(httpGet); + try { + assertEquals(200, status.getStatusLine().getStatusCode()); + } finally { + IOUtils.closeQuietly(status); + } + + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2"); + httpGet.addHeader(Constants.HEADER_IF_MODIFIED_SINCE, DateUtils.formatDate(new InstantDt("2012-01-01T10:00:00Z").getValue())); + status = ourClient.execute(httpGet); + try { + assertEquals(304, status.getStatusLine().getStatusCode()); + } finally { + IOUtils.closeQuietly(status); + } + + } /** * See #302 */ @Test public void testAddProfile() throws Exception { - ourCtx.setAddProfileTagWhenEncoding(AddProfileTagEnum.ALWAYS); + ourCtx.setAddProfileTagWhenEncoding(AddProfileTagEnum.ONLY_FOR_CUSTOM); HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123?_format=xml"); HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent(), Constants.CHARSET_UTF8); IOUtils.closeQuietly(status.getEntity().getContent()); assertEquals(200, status.getStatusLine().getStatusCode()); assertThat(responseContent, containsString("p1ReadValue")); assertThat(responseContent, containsString("p1ReadId")); - assertEquals("", responseContent); + assertEquals("", responseContent); ourLog.info(responseContent); @@ -87,7 +107,7 @@ public class ReadDstu2Test { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123&_format=xml"); HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent(), Constants.CHARSET_UTF8); IOUtils.closeQuietly(status.getEntity().getContent()); ourLog.info(responseContent); @@ -95,7 +115,7 @@ public class ReadDstu2Test { assertEquals(200, status.getStatusLine().getStatusCode()); assertThat(responseContent, containsString("p1ReadValue")); assertThat(responseContent, containsString("p1ReadId")); - assertEquals("", responseContent); + assertEquals("", responseContent); } /** @@ -103,18 +123,18 @@ public class ReadDstu2Test { */ @Test public void testReadJson() throws Exception { - ourCtx.setAddProfileTagWhenEncoding(AddProfileTagEnum.ALWAYS); + ourCtx.setAddProfileTagWhenEncoding(AddProfileTagEnum.ONLY_FOR_CUSTOM); HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123?_format=json"); HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent(), Constants.CHARSET_UTF8); ourLog.info(responseContent); IOUtils.closeQuietly(status.getEntity().getContent()); assertEquals(200, status.getStatusLine().getStatusCode()); assertThat(responseContent, containsString("p1ReadValue")); assertThat(responseContent, containsString("p1ReadId")); - assertThat(responseContent, containsString("\"meta\":{\"profile\":[\"http://foo_profile\"]}")); + assertThat(responseContent, containsString("\"meta\":{\"lastUpdated\":\"2012-01-01T12:12:12Z\",\"profile\":[\"http://foo_profile\"]}")); } /** @@ -124,7 +144,7 @@ public class ReadDstu2Test { public void testReadXml() throws Exception { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123&_format=xml"); HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent(), Constants.CHARSET_UTF8); IOUtils.closeQuietly(status.getEntity().getContent()); assertEquals(200, status.getStatusLine().getStatusCode()); @@ -140,13 +160,13 @@ public class ReadDstu2Test { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123/_history/1"); HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent(), Constants.CHARSET_UTF8); IOUtils.closeQuietly(status.getEntity().getContent()); assertEquals(200, status.getStatusLine().getStatusCode()); assertThat(responseContent, containsString("p1ReadValue")); assertThat(responseContent, containsString("p1ReadId")); - assertEquals("", responseContent); + assertEquals("", responseContent); ourLog.info(responseContent); @@ -196,6 +216,8 @@ public class ReadDstu2Test { public Patient read(@IdParam IdDt theId) { ourLastId = theId; Patient p1 = new MyPatient(); + ResourceMetadataKeyEnum.UPDATED.put(p1, new InstantDt("2012-01-01T12:12:12Z")); + p1.setId("p1ReadId"); p1.addIdentifier().setValue("p1ReadValue"); if (ourInitializeProfileList) { diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/ReadDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/ReadDstu3Test.java index faf5b414845..1dd5145a520 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/ReadDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/ReadDstu3Test.java @@ -1,46 +1,31 @@ package ca.uhn.fhir.rest.server; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.stringContainsInOrder; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.TimeUnit; import org.apache.commons.io.IOUtils; import org.apache.http.HttpResponse; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; -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.dstu3.model.DateType; -import org.hl7.fhir.dstu3.model.IdType; -import org.hl7.fhir.dstu3.model.Patient; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; +import org.hl7.fhir.dstu3.model.*; +import org.junit.*; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.rest.annotation.Create; +import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Read; -import ca.uhn.fhir.rest.annotation.ResourceParam; -import ca.uhn.fhir.rest.annotation.Search; -import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.client.MyPatientWithExtensions; -import ca.uhn.fhir.util.PortUtil; -import ca.uhn.fhir.util.TestUtil; +import ca.uhn.fhir.util.*; public class ReadDstu3Test { private static CloseableHttpClient ourClient; @@ -50,7 +35,6 @@ public class ReadDstu3Test { private static int ourPort; private static Server ourServer; - @Test public void testRead() throws Exception { @@ -66,20 +50,43 @@ public class ReadDstu3Test { assertEquals("http://localhost:" + ourPort + "/Patient/2/_history/2", status.getFirstHeader(Constants.HEADER_LOCATION).getValue()); assertEquals(null, status.getFirstHeader(Constants.HEADER_CONTENT_LOCATION)); - //@formatter:off assertThat(responseContent, stringContainsInOrder( - "", - "", - "", - "", - "", - "", - "", - "", - "")); - //@formatter:on + "", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "")); } + @Test + public void testIfModifiedSince() throws Exception { + + CloseableHttpResponse status; + HttpGet httpGet; + + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2"); + httpGet.addHeader(Constants.HEADER_IF_MODIFIED_SINCE, DateUtils.formatDate(new InstantDt("2012-01-01T13:00:00Z").getValue())); + status = ourClient.execute(httpGet); + try { + assertEquals(200, status.getStatusLine().getStatusCode()); + } finally { + IOUtils.closeQuietly(status); + } + + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2"); + httpGet.addHeader(Constants.HEADER_IF_MODIFIED_SINCE, DateUtils.formatDate(new InstantDt("2012-01-01T10:00:00Z").getValue())); + status = ourClient.execute(httpGet); + try { + assertEquals(304, status.getStatusLine().getStatusCode()); + } finally { + IOUtils.closeQuietly(status); + } + + } @AfterClass public static void afterClassClearContext() throws Exception { @@ -117,9 +124,10 @@ public class ReadDstu3Test { return Patient.class; } - @Read(version=true) + @Read(version = true) public MyPatientWithExtensions read(@IdParam IdType theIdParam) { MyPatientWithExtensions p0 = new MyPatientWithExtensions(); + p0.getMeta().getLastUpdatedElement().setValueAsString("2012-01-01T12:12:12Z"); p0.setId(theIdParam); if (theIdParam.hasVersionIdPart() == false) { p0.setIdElement(p0.getIdElement().withVersion("2")); @@ -128,7 +136,6 @@ public class ReadDstu3Test { return p0; } - } } diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 70397ab8923..8b45197c9ad 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -101,6 +101,10 @@ Fix issue where the JSON parser sometimes did not encode DSTU3 extensions on the root of a resource which have a value of type reference. + + Server now respects the If-Modified-Since header and will return an HTTP 304 if appropriate + for read operations. +