Server now respects the If-Modified-Since header for read operations

This commit is contained in:
James Agnew 2017-05-17 14:57:05 -04:00
parent a92d80d860
commit 45041830bc
6 changed files with 142 additions and 96 deletions

View File

@ -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<Object> {
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<IBaseResource> 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<IBaseResource> 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;
}

View File

@ -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;

View File

@ -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 {

View File

@ -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("<Patient xmlns=\"http://hl7.org/fhir\"><id value=\"p1ReadId\"/><meta><profile value=\"http://foo_profile\"/></meta><identifier><value value=\"p1ReadValue\"/></identifier></Patient>", responseContent);
assertEquals("<Patient xmlns=\"http://hl7.org/fhir\"><id value=\"p1ReadId\"/><meta><lastUpdated value=\"2012-01-01T12:12:12Z\"/><profile value=\"http://foo_profile\"/></meta><identifier><value value=\"p1ReadValue\"/></identifier></Patient>", 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("<Patient xmlns=\"http://hl7.org/fhir\"><id value=\"p1ReadId\"/><meta><profile value=\"http://foo\"/><profile value=\"http://foo_profile\"/></meta><identifier><value value=\"p1ReadValue\"/></identifier></Patient>", responseContent);
assertEquals("<Patient xmlns=\"http://hl7.org/fhir\"><id value=\"p1ReadId\"/><meta><lastUpdated value=\"2012-01-01T12:12:12Z\"/><profile value=\"http://foo\"/><profile value=\"http://foo_profile\"/></meta><identifier><value value=\"p1ReadValue\"/></identifier></Patient>", 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("<Patient xmlns=\"http://hl7.org/fhir\"><id value=\"p1ReadId\"/><meta><profile value=\"http://foo_profile\"/></meta><identifier><value value=\"p1ReadValue\"/></identifier></Patient>", responseContent);
assertEquals("<Patient xmlns=\"http://hl7.org/fhir\"><id value=\"p1ReadId\"/><meta><lastUpdated value=\"2012-01-01T12:12:12Z\"/><profile value=\"http://foo_profile\"/></meta><identifier><value value=\"p1ReadValue\"/></identifier></Patient>", 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) {

View File

@ -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(
"<Patient xmlns=\"http://hl7.org/fhir\">",
"<id value=\"2\"/>",
"<meta>",
"<profile value=\"http://example.com/StructureDefinition/patient_with_extensions\"/>",
"</meta>",
"<modifierExtension url=\"http://example.com/ext/date\">",
"<valueDate value=\"2011-01-01\"/>",
"</modifierExtension>",
"</Patient>"));
//@formatter:on
"<Patient xmlns=\"http://hl7.org/fhir\">",
" <id value=\"2\"/>",
" <meta>",
" <profile value=\"http://example.com/StructureDefinition/patient_with_extensions\"/>",
" </meta>",
" <modifierExtension url=\"http://example.com/ext/date\">",
" <valueDate value=\"2011-01-01\"/>",
" </modifierExtension>",
"</Patient>"));
}
@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;
}
}
}

View File

@ -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.
</action>
<action type="add">
Server now respects the If-Modified-Since header and will return an HTTP 304 if appropriate
for read operations.
</action>
</release>
<release version="2.4" date="2017-04-19">
<action type="add">