Sere up Binary resources as binary content even if the browser puts

application/xml in the Accept header
This commit is contained in:
James Agnew 2016-03-18 18:38:44 +01:00
parent c8173810f4
commit 82c6d82444
7 changed files with 166 additions and 15 deletions

View File

@ -1,5 +1,7 @@
package ca.uhn.fhir.rest.server; package ca.uhn.fhir.rest.server;
import java.util.Collections;
/* /*
* #%L * #%L
* HAPI FHIR - Core Library * HAPI FHIR - Core Library
@ -21,6 +23,7 @@ package ca.uhn.fhir.rest.server;
*/ */
import java.util.HashMap; import java.util.HashMap;
import java.util.Map;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.parser.IParser;
@ -43,8 +46,8 @@ public enum EncodingEnum {
; ;
private static HashMap<String, EncodingEnum> ourContentTypeToEncoding; private static Map<String, EncodingEnum> ourContentTypeToEncoding;
private static HashMap<String, EncodingEnum> ourContentTypeToEncodingStrict; private static Map<String, EncodingEnum> ourContentTypeToEncodingStrict;
static { static {
ourContentTypeToEncoding = new HashMap<String, EncodingEnum>(); ourContentTypeToEncoding = new HashMap<String, EncodingEnum>();
@ -54,7 +57,7 @@ public enum EncodingEnum {
} }
// Add before we add the lenient ones // Add before we add the lenient ones
ourContentTypeToEncodingStrict = new HashMap<String, EncodingEnum>(ourContentTypeToEncoding); ourContentTypeToEncodingStrict = Collections.unmodifiableMap(new HashMap<String, EncodingEnum>(ourContentTypeToEncoding));
/* /*
* These are wrong, but we add them just to be tolerant of other * These are wrong, but we add them just to be tolerant of other
@ -116,6 +119,18 @@ public enum EncodingEnum {
return ourContentTypeToEncodingStrict.get(theContentType); return ourContentTypeToEncodingStrict.get(theContentType);
} }
/**
* Returns a map containing the encoding for a given content type, or <code>null</code> if no encoding
* is found.
* <p>
* <b>This method is NOT lenient!</b> Things like "application/xml" will return <code>null</code>
* </p>
* @see #forContentType(String)
*/
public static Map<String, EncodingEnum> getContentTypeToEncodingStrict() {
return ourContentTypeToEncodingStrict;
}
public String getFormatContentType() { public String getFormatContentType() {
return myFormatContentType; return myFormatContentType;
} }

View File

@ -251,6 +251,20 @@ public class RestfulServerUtils {
} }
} }
/*
* Some browsers (e.g. FF) request "application/xml" in their Accept header,
* and we generally want to treat this as a preference for FHIR XML even if
* it's not the FHIR version of the CT, which should be "application/xml+fhir".
*
* When we're serving up Binary resources though, we are a bit more strict,
* since Binary is supposed to use native content types unless the client has
* explicitly requested FHIR.
*/
Map<String, EncodingEnum> contentTypeToEncoding = Constants.FORMAT_VAL_TO_ENCODING;
if ("Binary".equals(theReq.getResourceName())) {
contentTypeToEncoding = EncodingEnum.getContentTypeToEncodingStrict();
}
/* /*
* The Accept header is kind of ridiculous, e.g. * The Accept header is kind of ridiculous, e.g.
*/ */
@ -288,12 +302,12 @@ public class RestfulServerUtils {
EncodingEnum encoding; EncodingEnum encoding;
if (endSpaceIndex == -1) { if (endSpaceIndex == -1) {
if (startSpaceIndex == 0) { if (startSpaceIndex == 0) {
encoding = Constants.FORMAT_VAL_TO_ENCODING.get(nextToken); encoding = contentTypeToEncoding.get(nextToken);
} else { } else {
encoding = Constants.FORMAT_VAL_TO_ENCODING.get(nextToken.substring(startSpaceIndex)); encoding = contentTypeToEncoding.get(nextToken.substring(startSpaceIndex));
} }
} else { } else {
encoding = Constants.FORMAT_VAL_TO_ENCODING.get(nextToken.substring(startSpaceIndex, endSpaceIndex)); encoding = contentTypeToEncoding.get(nextToken.substring(startSpaceIndex, endSpaceIndex));
String remaining = nextToken.substring(endSpaceIndex + 1); String remaining = nextToken.substring(endSpaceIndex + 1);
StringTokenizer qualifierTok = new StringTokenizer(remaining, ";"); StringTokenizer qualifierTok = new StringTokenizer(remaining, ";");
while (qualifierTok.hasMoreTokens()) { while (qualifierTok.hasMoreTokens()) {

View File

@ -207,6 +207,16 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter {
return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse); return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse);
} }
/*
* Not binary
*/
if ("Binary".equals(theRequestDetails.getResourceName())) {
return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse);
}
/*
* Request for _raw
*/
String[] rawParamValues = theRequestDetails.getParameters().get(PARAM_RAW); String[] rawParamValues = theRequestDetails.getParameters().get(PARAM_RAW);
if (rawParamValues != null && rawParamValues.length > 0 && rawParamValues[0].equals(PARAM_RAW_TRUE)) { if (rawParamValues != null && rawParamValues.length > 0 && rawParamValues[0].equals(PARAM_RAW_TRUE)) {
return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse); return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse);

View File

@ -38,9 +38,6 @@ import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.util.PortUtil; import ca.uhn.fhir.util.PortUtil;
/**
* Created by dsotnikov on 2/25/2014.
*/
public class BinaryTest { public class BinaryTest {
private static CloseableHttpClient ourClient; private static CloseableHttpClient ourClient;

View File

@ -3,6 +3,7 @@ package ca.uhn.fhir.rest.server;
import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import java.util.Collections; import java.util.Collections;
@ -126,17 +127,50 @@ public class BinaryDstu2Test {
} }
@Test @Test
public void testRead() throws Exception { public void testBinaryReadAcceptMissing() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Binary/foo"); HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Binary/foo");
HttpResponse status = ourClient.execute(httpGet); HttpResponse status = ourClient.execute(httpGet);
byte[] responseContent = IOUtils.toByteArray(status.getEntity().getContent()); byte[] responseContent = IOUtils.toByteArray(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent()); IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals("foo", status.getFirstHeader("content-type").getValue()); assertEquals("foo", status.getFirstHeader("content-type").getValue());
assertEquals("Attachment;", status.getFirstHeader("Content-Disposition").getValue());
assertArrayEquals(new byte[] { 1, 2, 3, 4 }, responseContent); assertArrayEquals(new byte[] { 1, 2, 3, 4 }, responseContent);
} }
@Test
public void testBinaryReadAcceptBrowser() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Binary/foo");
httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1");
httpGet.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
HttpResponse status = ourClient.execute(httpGet);
byte[] responseContent = IOUtils.toByteArray(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals("foo", status.getFirstHeader("content-type").getValue());
assertEquals("Attachment;", status.getFirstHeader("Content-Disposition").getValue());
assertArrayEquals(new byte[] { 1, 2, 3, 4 }, responseContent);
}
@Test
public void testBinaryReadAcceptFhirJson() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Binary/foo");
httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1");
httpGet.addHeader("Accept", Constants.CT_FHIR_JSON);
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals(Constants.CT_FHIR_JSON + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase());
assertNull(status.getFirstHeader("Content-Disposition"));
assertEquals("{\"resourceType\":\"Binary\",\"id\":\"1\",\"contentType\":\"foo\",\"content\":\"AQIDBA==\"}", responseContent);
}
@Test @Test
public void testSearchJson() throws Exception { public void testSearchJson() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Binary?_pretty=true&_format=json"); HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Binary?_pretty=true&_format=json");
@ -200,9 +234,6 @@ public class BinaryDstu2Test {
} }
/**
* Created by dsotnikov on 2/25/2014.
*/
public static class ResourceProvider implements IResourceProvider { public static class ResourceProvider implements IResourceProvider {
@Create @Create

View File

@ -3,8 +3,10 @@ package ca.uhn.fhir.rest.server.interceptor;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.stringContainsInOrder; import static org.hamcrest.Matchers.stringContainsInOrder;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@ -23,6 +25,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.CloseableHttpClient;
@ -42,6 +45,7 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu2.composite.HumanNameDt; import ca.uhn.fhir.model.dstu2.composite.HumanNameDt;
import ca.uhn.fhir.model.dstu2.composite.IdentifierDt; import ca.uhn.fhir.model.dstu2.composite.IdentifierDt;
import ca.uhn.fhir.model.dstu2.resource.Binary;
import ca.uhn.fhir.model.dstu2.resource.OperationOutcome; import ca.uhn.fhir.model.dstu2.resource.OperationOutcome;
import ca.uhn.fhir.model.dstu2.resource.OperationOutcome.Issue; import ca.uhn.fhir.model.dstu2.resource.OperationOutcome.Issue;
import ca.uhn.fhir.model.dstu2.resource.Organization; import ca.uhn.fhir.model.dstu2.resource.Organization;
@ -343,6 +347,51 @@ public class ResponseHighlightingInterceptorTest {
assertThat(responseContent, not(containsString("entry"))); assertThat(responseContent, not(containsString("entry")));
} }
@Test
public void testBinaryReadAcceptMissing() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Binary/foo");
HttpResponse status = ourClient.execute(httpGet);
byte[] responseContent = IOUtils.toByteArray(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals("foo", status.getFirstHeader("content-type").getValue());
assertEquals("Attachment;", status.getFirstHeader("Content-Disposition").getValue());
assertArrayEquals(new byte[] { 1, 2, 3, 4 }, responseContent);
}
@Test
public void testBinaryReadAcceptBrowser() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Binary/foo");
httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1");
httpGet.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
HttpResponse status = ourClient.execute(httpGet);
byte[] responseContent = IOUtils.toByteArray(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals("foo", status.getFirstHeader("content-type").getValue());
assertEquals("Attachment;", status.getFirstHeader("Content-Disposition").getValue());
assertArrayEquals(new byte[] { 1, 2, 3, 4 }, responseContent);
}
@Test
public void testBinaryReadAcceptFhirJson() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Binary/foo");
httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1");
httpGet.addHeader("Accept", Constants.CT_FHIR_JSON);
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals(Constants.CT_FHIR_JSON + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase());
assertNull(status.getFirstHeader("Content-Disposition"));
assertEquals("{\"resourceType\":\"Binary\",\"id\":\"1\",\"contentType\":\"foo\",\"content\":\"AQIDBA==\"}", responseContent);
}
@BeforeClass @BeforeClass
public static void beforeClass() throws Exception { public static void beforeClass() throws Exception {
ourPort = PortUtil.findFreePort(); ourPort = PortUtil.findFreePort();
@ -353,7 +402,7 @@ public class ResponseHighlightingInterceptorTest {
ServletHandler proxyHandler = new ServletHandler(); ServletHandler proxyHandler = new ServletHandler();
ourServlet = new RestfulServer(ourCtx); ourServlet = new RestfulServer(ourCtx);
ourServlet.registerInterceptor(new ResponseHighlighterInterceptor()); ourServlet.registerInterceptor(new ResponseHighlighterInterceptor());
ourServlet.setResourceProviders(patientProvider); ourServlet.setResourceProviders(patientProvider, new DummyBinaryResourceProvider());
ourServlet.setBundleInclusionRule(BundleInclusionRule.BASED_ON_RESOURCE_PRESENCE); ourServlet.setBundleInclusionRule(BundleInclusionRule.BASED_ON_RESOURCE_PRESENCE);
ServletHolder servletHolder = new ServletHolder(ourServlet); ServletHolder servletHolder = new ServletHolder(ourServlet);
proxyHandler.addServletWithMapping(servletHolder, "/*"); proxyHandler.addServletWithMapping(servletHolder, "/*");
@ -367,6 +416,34 @@ public class ResponseHighlightingInterceptorTest {
} }
public static class DummyBinaryResourceProvider implements IResourceProvider {
@Override
public Class<? extends IResource> getResourceType() {
return Binary.class;
}
@Read
public Binary read(@IdParam IdDt theId) {
Binary retVal = new Binary();
retVal.setId("1");
retVal.setContent(new byte[] { 1, 2, 3, 4 });
retVal.setContentType(theId.getIdPart());
return retVal;
}
@Search
public List<Binary> search() {
Binary retVal = new Binary();
retVal.setId("1");
retVal.setContent(new byte[] { 1, 2, 3, 4 });
retVal.setContentType("text/plain");
return Collections.singletonList(retVal);
}
}
/** /**
* Created by dsotnikov on 2/25/2014. * Created by dsotnikov on 2/25/2014.
*/ */

View File

@ -253,7 +253,14 @@
so that different tables use different sequences so that different tables use different sequences
to generate their indexes, resulting in more sequential to generate their indexes, resulting in more sequential
resource IDs being assigned by the server resource IDs being assigned by the server
<action> </action>
<action type="fix">
Server now correctly serves up Binary resources
using their native content type (instead of as a
FHIR resource) if the request contains an accept
header containing "application/xml" as some browsers
do.
</action>
</release> </release>
<release version="1.4" date="2016-02-04"> <release version="1.4" date="2016-02-04">
<action type="add"> <action type="add">