Correct handling of Binary resources in client and server where the

binary contains embedded FHIR content
This commit is contained in:
James Agnew 2016-02-25 14:31:26 -08:00
parent 4a26064cd6
commit d383b402d1
12 changed files with 377 additions and 67 deletions

View File

@ -25,6 +25,29 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import ca.uhn.fhir.rest.server.IResourceProvider;
/**
* Denotes a parameter for a REST method which will contain the resource actually
* being created/updated/etc in an operation which contains a resource in the HTTP request.
* <p>
* For example, in a {@link Create} operation the method parameter annotated with this
* annotation will contain the actual resource being created.
* </p>
* <p>
* Parameters with this annotation should typically be of the type of resource being
* operated on (see below for an exception when raw data is required). For example, in a
* {@link IResourceProvider} for Patient resources, the parameter annotated with this
* annotation should be of type Patient.
* </p>
* <p>
* Note that in servers it is also acceptable to have parameters with this annotation
* which are of type {@link String} or of type <code>byte[]</code>. Parameters of
* these types will contain the raw/unparsed HTTP request body. It is fine to
* have multiple parameters with this annotation, so you can have one parameter
* which accepts the parsed resource, and another which accepts the raw request.
* </p>
*/
@Target(value=ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResourceParam {

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.rest.method;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/*
@ -56,6 +57,7 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
* @author Doug Martin (Regenstrief Center for Biomedical Informatics)
*/
abstract class BaseHttpClientInvocationWithContents extends BaseHttpClientInvocation {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHttpClientInvocationWithContents.class);
private final Bundle myBundle;
private final BundleTypeEnum myBundleType;
@ -208,21 +210,24 @@ abstract class BaseHttpClientInvocationWithContents extends BaseHttpClientInvoca
if (myResource != null && IBaseBinary.class.isAssignableFrom(myResource.getClass())) {
IBaseBinary binary = (IBaseBinary) myResource;
/*
* Note: Be careful about changing which constructor we use for ByteArrayEntity,
* as Android's version of HTTPClient doesn't support the newer ones for
* whatever reason.
*/
ByteArrayEntity entity = new ByteArrayEntity(binary.getContent());
HttpRequestBase retVal = createRequest(url, entity);
addMatchHeaders(retVal, url);
super.addHeadersToRequest(retVal, null);
if (isNotBlank(binary.getContentType())) {
retVal.addHeader(Constants.HEADER_CONTENT_TYPE, binary.getContentType());
if (isBlank(binary.getContentType()) || EncodingEnum.forContentTypeStrict(binary.getContentType()) != null) {
ourLog.trace("Binary has Content-Type {}, encoding as a FHIR resource instead of raw", binary.getContentType());
} else {
/*
* Note: Be careful about changing which constructor we use for ByteArrayEntity,
* as Android's version of HTTPClient doesn't support the newer ones for
* whatever reason.
*/
ByteArrayEntity entity = new ByteArrayEntity(binary.getContent());
HttpRequestBase retVal = createRequest(url, entity);
addMatchHeaders(retVal, url);
super.addHeadersToRequest(retVal, null);
if (isNotBlank(binary.getContentType())) {
retVal.addHeader(Constants.HEADER_CONTENT_TYPE, binary.getContentType());
}
return retVal;
}
return retVal;
}
IParser parser;

View File

@ -117,7 +117,7 @@ abstract class BaseOutcomeReturningMethodBindingWithResourceParam extends BaseOu
@Override
public BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException {
IResource resource = (IResource) theArgs[myResourceParameterIndex];
IResource resource = (IResource) theArgs[myResourceParameterIndex]; // TODO: use IBaseResource
if (resource == null) {
throw new NullPointerException("Resource can not be null");
}

View File

@ -451,10 +451,20 @@ public class MethodUtil {
mode = Mode.RESOURCE;
} else if (String.class.equals(parameterType)) {
mode = ResourceParameter.Mode.BODY;
} else if (byte[].class.equals(parameterType)) {
mode = ResourceParameter.Mode.BODY_BYTE_ARRAY;
} else if (EncodingEnum.class.equals(parameterType)) {
mode = Mode.ENCODING;
} else {
throw new ConfigurationException("Method '" + theMethod.getName() + "' is annotated with @" + ResourceParam.class.getSimpleName() + " but has a type that is not an implemtation of " + IResource.class.getCanonicalName());
StringBuilder b = new StringBuilder();
b.append("Method '");
b.append(theMethod.getName());
b.append("' is annotated with @");
b.append(ResourceParam.class.getSimpleName());
b.append(" but has a type that is not an implemtation of ");
b.append(IBaseResource.class.getCanonicalName());
b.append(" or String or byte[]");
throw new ConfigurationException(b.toString());
}
param = new ResourceParameter((Class<? extends IResource>) parameterType, theProvider, mode);
} else if (nextAnnotation instanceof IdParam || nextAnnotation instanceof VersionIdParam) {

View File

@ -107,8 +107,10 @@ public class ResourceParameter implements IParameter {
return IOUtils.toString(createRequestReader(theRequest));
} catch (IOException e) {
// Shouldn't happen since we're reading from a byte array
throw new InternalErrorException("Failed to load request");
throw new InternalErrorException("Failed to load request", e);
}
case BODY_BYTE_ARRAY:
return theRequest.loadRequestContents();
case ENCODING:
return RestfulServerUtils.determineRequestEncoding(theRequest);
case RESOURCE:
@ -209,23 +211,27 @@ public class ResourceParameter implements IParameter {
}
public static IBaseResource parseResourceFromRequest(RequestDetails theRequest, BaseMethodBinding<?> theMethodBinding, Class<? extends IBaseResource> theResourceType) {
IBaseResource retVal;
IBaseResource retVal = null;
if (IBaseBinary.class.isAssignableFrom(theResourceType)) {
FhirContext ctx = theRequest.getServer().getFhirContext();
String ct = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE);
if (EncodingEnum.forContentTypeStrict(ct) == null) {
FhirContext ctx = theRequest.getServer().getFhirContext();
IBaseBinary binary = (IBaseBinary) ctx.getResourceDefinition("Binary").newInstance();
binary.setContentType(ct);
binary.setContent(theRequest.loadRequestContents());
retVal = binary;
} else {
}
}
if (retVal == null) {
retVal = loadResourceFromRequest(theRequest, theMethodBinding, theResourceType);
}
return retVal;
}
public enum Mode {
BODY, ENCODING, RESOURCE
BODY, BODY_BYTE_ARRAY, ENCODING, RESOURCE
}
}

View File

@ -44,6 +44,7 @@ public enum EncodingEnum {
;
private static HashMap<String, EncodingEnum> ourContentTypeToEncoding;
private static HashMap<String, EncodingEnum> ourContentTypeToEncodingStrict;
static {
ourContentTypeToEncoding = new HashMap<String, EncodingEnum>();
@ -51,6 +52,9 @@ public enum EncodingEnum {
ourContentTypeToEncoding.put(next.getBundleContentType(), next);
ourContentTypeToEncoding.put(next.getResourceContentType(), next);
}
// Add before we add the lenient ones
ourContentTypeToEncodingStrict = new HashMap<String, EncodingEnum>(ourContentTypeToEncoding);
/*
* These are wrong, but we add them just to be tolerant of other
@ -88,10 +92,30 @@ public enum EncodingEnum {
return myResourceContentType;
}
/**
* Returns the encoding for a given content type, or <code>null</code> if no encoding
* is found.
* <p>
* <b>This method is lenient!</b> Things like "application/xml" will return {@link EncodingEnum#XML}
* even if the "+fhir" part is missing from the expected content type.
* </p>
*/
public static EncodingEnum forContentType(String theContentType) {
return ourContentTypeToEncoding.get(theContentType);
}
/**
* Returns 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 EncodingEnum forContentTypeStrict(String theContentType) {
return ourContentTypeToEncodingStrict.get(theContentType);
}
public String getFormatContentType() {
return myFormatContentType;
}

View File

@ -517,7 +517,6 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
protected void handleRequest(RequestTypeEnum theRequestType, HttpServletRequest theRequest, HttpServletResponse theResponse) throws ServletException, IOException {
String fhirServerBase = null;
boolean requestIsBrowser = requestIsBrowser(theRequest);
ServletRequestDetails requestDetails = new ServletRequestDetails();
requestDetails.setServer(this);
requestDetails.setRequestType(theRequestType);
@ -1393,9 +1392,4 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
return nextString.length() > 0 && (nextString.charAt(0) == '_' || nextString.charAt(0) == '$');
}
public static boolean requestIsBrowser(HttpServletRequest theRequest) {
String userAgent = theRequest.getHeader("User-Agent");
return userAgent != null && userAgent.contains("Mozilla");
}
}

View File

@ -1,7 +1,9 @@
package ca.uhn.fhir.rest.client;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.ByteArrayInputStream;
@ -17,11 +19,9 @@ import org.apache.http.message.BasicStatusLine;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.dstu.resource.Binary;
import ca.uhn.fhir.model.dstu.resource.Conformance;
import ca.uhn.fhir.model.dstu.resource.Patient;
@ -48,7 +48,7 @@ public class BinaryClientTest {
httpClient = mock(HttpClient.class, new ReturnsDeepStubs());
ctx.getRestfulClientFactory().setHttpClient(httpClient);
ctx.getRestfulClientFactory().setServerValidationModeEnum(ServerValidationModeEnum.NEVER);
ctx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
httpResponse = mock(HttpResponse.class, new ReturnsDeepStubs());
}
@ -59,7 +59,7 @@ public class BinaryClientTest {
when(httpClient.execute(capt.capture())).thenReturn(httpResponse);
when(httpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(httpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", "foo/bar"));
when(httpResponse.getEntity().getContent()).thenReturn(new ByteArrayInputStream(new byte[] {1,2,3,4}));
when(httpResponse.getEntity().getContent()).thenReturn(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 }));
IClient client = ctx.newRestfulClient(IClient.class, "http://foo");
Binary resp = client.read(new IdDt("http://foo/Patient/123"));
@ -67,24 +67,17 @@ public class BinaryClientTest {
assertEquals(HttpGet.class, capt.getValue().getClass());
HttpGet get = (HttpGet) capt.getValue();
assertEquals("http://foo/Binary/123", get.getURI().toString());
assertEquals("foo/bar", resp.getContentType());
assertArrayEquals(new byte[] { 1, 2, 3, 4 }, resp.getContent());
}
public static void main(String[] args) {
IClient c = Mockito.mock(IClient.class, new ReturnsDeepStubs());
}
@Test
public void testCreate() throws Exception {
Binary res = new Binary();
res.setContent(new byte[] { 1, 2, 3, 4 });
res.setContentType("text/plain");
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(httpClient.execute(capt.capture())).thenReturn(httpResponse);
when(httpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK"));
@ -92,29 +85,23 @@ public class BinaryClientTest {
when(httpResponse.getEntity().getContent()).thenReturn(new ByteArrayInputStream(new byte[] {}));
IClient client = ctx.newRestfulClient(IClient.class, "http://foo");
MethodOutcome resp = client.create(res);
client.create(res);
assertEquals(HttpPost.class, capt.getValue().getClass());
HttpPost post = (HttpPost) capt.getValue();
assertEquals("http://foo/Binary", post.getURI().toString());
assertEquals("text/plain", post.getEntity().getContentType().getValue());
assertEquals("text/plain", capt.getValue().getFirstHeader("Content-Type").getValue());
assertArrayEquals(new byte[] { 1, 2, 3, 4 }, IOUtils.toByteArray(post.getEntity().getContent()));
}
private String createBundle() {
return ctx.newXmlParser().encodeBundleToString(new Bundle());
}
private interface IClient extends IBasicClient {
@Read(type=Binary.class)
@Read(type = Binary.class)
public Binary read(@IdParam IdDt theBinary);
@Create(type=Binary.class)
@Create(type = Binary.class)
public MethodOutcome create(@ResourceParam Binary theBinary);
}

View File

@ -21,6 +21,8 @@ import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicStatusLine;
import org.hl7.fhir.dstu3.model.Binary;
import org.hl7.fhir.dstu3.model.Conformance;
import org.hl7.fhir.dstu3.model.OperationOutcome;
import org.hl7.fhir.dstu3.model.Patient;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
@ -37,20 +39,15 @@ import ca.uhn.fhir.util.VersionUtil;
public class GenericClientDstu3Test {
private static FhirContext ourCtx;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(GenericClientDstu3Test.class);
private HttpClient myHttpClient;
private HttpResponse myHttpResponse;
private int myResponseCount = 0;
@Before
public void before() {
myHttpClient = mock(HttpClient.class, new ReturnsDeepStubs());
ourCtx.getRestfulClientFactory().setHttpClient(myHttpClient);
ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
myHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs());
myResponseCount = 0;
}
private byte[] extractBodyAsByteArray(ArgumentCaptor<HttpUriRequest> capt) throws IOException {
@ -58,6 +55,10 @@ public class GenericClientDstu3Test {
return body;
}
private String extractBodyAsString(ArgumentCaptor<HttpUriRequest> capt) throws IOException {
String body = IOUtils.toString(((HttpEntityEnclosingRequestBase) capt.getAllValues().get(0)).getEntity().getContent(), "UTF-8");
return body;
}
@Test
public void testUserAgentForConformance() throws Exception {
@ -108,20 +109,99 @@ public class GenericClientDstu3Test {
Binary bin = new Binary();
bin.setContentType("application/foo");
bin.setContent(new byte[] {0, 1, 2, 3, 4});
bin.setContent(new byte[] { 0, 1, 2, 3, 4 });
client.create().resource(bin).execute();
ourLog.info(Arrays.asList(capt.getAllValues().get(0).getAllHeaders()).toString());
assertEquals("http://example.com/fhir/Binary", capt.getAllValues().get(0).getURI().toASCIIString());
validateUserAgent(capt);
assertEquals("application/foo", capt.getAllValues().get(0).getHeaders("Content-Type")[0].getValue());
assertEquals(Constants.HEADER_ACCEPT_VALUE_XML_OR_JSON, capt.getAllValues().get(0).getHeaders("Accept")[0].getValue());
assertArrayEquals(new byte[] {0,1,2,3,4}, extractBodyAsByteArray(capt));
assertArrayEquals(new byte[] { 0, 1, 2, 3, 4 }, extractBodyAsByteArray(capt));
}
@Test
public void testBinaryCreateWithNoContentType() throws Exception {
IParser p = ourCtx.newXmlParser();
OperationOutcome conf = new OperationOutcome();
conf.getText().setDivAsString("OK!");
final String respString = p.encodeResourceToString(conf);
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<ReaderInputStream>() {
@Override
public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable {
return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8"));
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
Binary bin = new Binary();
bin.setContent(new byte[] { 0, 1, 2, 3, 4 });
client.create().resource(bin).execute();
ourLog.info(Arrays.asList(capt.getAllValues().get(0).getAllHeaders()).toString());
assertEquals("http://example.com/fhir/Binary", capt.getAllValues().get(0).getURI().toASCIIString());
validateUserAgent(capt);
assertEquals("application/xml+fhir;charset=utf-8", capt.getAllValues().get(0).getHeaders("Content-Type")[0].getValue().toLowerCase().replace(" ", ""));
assertEquals(Constants.CT_FHIR_XML, capt.getAllValues().get(0).getHeaders("Accept")[0].getValue());
assertArrayEquals(new byte[] { 0, 1, 2, 3, 4 }, ourCtx.newXmlParser().parseResource(Binary.class, extractBodyAsString(capt)).getContent());
}
@Test
public void testBinaryCreateWithFhirContentType() throws Exception {
IParser p = ourCtx.newXmlParser();
OperationOutcome conf = new OperationOutcome();
conf.getText().setDivAsString("OK!");
final String respString = p.encodeResourceToString(conf);
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<ReaderInputStream>() {
@Override
public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable {
return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8"));
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
Patient pt = new Patient();
pt.getText().setDivAsString("A PATIENT");
Binary bin = new Binary();
bin.setContent(ourCtx.newJsonParser().encodeResourceToString(pt).getBytes("UTF-8"));
bin.setContentType(Constants.CT_FHIR_JSON);
client.create().resource(bin).execute();
ourLog.info(Arrays.asList(capt.getAllValues().get(0).getAllHeaders()).toString());
assertEquals("http://example.com/fhir/Binary", capt.getAllValues().get(0).getURI().toASCIIString());
validateUserAgent(capt);
assertEquals("application/xml+fhir;charset=utf-8", capt.getAllValues().get(0).getHeaders("Content-Type")[0].getValue().toLowerCase().replace(" ", ""));
assertEquals(Constants.CT_FHIR_XML, capt.getAllValues().get(0).getHeaders("Accept")[0].getValue());
Binary output = ourCtx.newXmlParser().parseResource(Binary.class, extractBodyAsString(capt));
assertEquals(Constants.CT_FHIR_JSON, output.getContentType());
Patient outputPt = (Patient) ourCtx.newJsonParser().parseResource(new String(output.getContent(), "UTF-8"));
assertEquals("<div>A PATIENT</div>", outputPt.getText().getDivAsString());
}
private void validateUserAgent(ArgumentCaptor<HttpUriRequest> capt) {
assertEquals(1, capt.getAllValues().get(0).getHeaders("User-Agent").length);
assertEquals(expectedUserAgent(), capt.getAllValues().get(0).getHeaders("User-Agent")[0].getValue());
@ -131,7 +211,6 @@ public class GenericClientDstu3Test {
return "HAPI-FHIR/" + VersionUtil.getVersion() + " (FHIR Client)";
}
@BeforeClass
public static void beforeClass() {
ourCtx = FhirContext.forDstu3();

View File

@ -0,0 +1,161 @@
package ca.uhn.fhir.rest.server;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import java.util.concurrent.TimeUnit;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ByteArrayEntity;
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.Binary;
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.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.util.PortUtil;
public class CreateBinaryDstu3Test {
private static CloseableHttpClient ourClient;
private static int ourPort;
private static FhirContext ourCtx = FhirContext.forDstu3();
private static Server ourServer;
private static Binary ourLastBinary;
private static String ourLastBinaryString;
private static byte[] ourLastBinaryBytes;
@Before
public void before() {
ourLastBinary = null;
ourLastBinaryBytes = null;
ourLastBinaryString = null;
}
@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());
}
@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.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);
}
/**
* 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});
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());
}
@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);
}
@AfterClass
public static void afterClass() throws Exception {
ourServer.stop();
}
@BeforeClass
public static void beforeClass() throws Exception {
ourPort = PortUtil.findFreePort();
ourServer = new Server(ourPort);
BinaryProvider binaryProvider = new BinaryProvider();
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer(ourCtx);
servlet.setResourceProviders(binaryProvider);
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
ourServer.start();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setConnectionManager(connectionManager);
ourClient = builder.build();
}
public static class BinaryProvider implements IResourceProvider {
@Override
public Class<? extends IBaseResource> getResourceType() {
return Binary.class;
}
@Create()
public MethodOutcome createBinary(@ResourceParam Binary theBinary, @ResourceParam String theBinaryString, @ResourceParam byte[] theBinaryBytes) {
ourLastBinary = theBinary;
ourLastBinaryString = theBinaryString;
ourLastBinaryBytes = theBinaryBytes;
return new MethodOutcome(new IdType("Binary/001/_history/002"));
}
}
}

View File

@ -84,6 +84,25 @@
User-Agent, etc.) Thanks to Peter Van Houte of Agfa Healthcare for
reporting!
</action>
<action type="fix">
Handling of Binary resources containing embedded FHIR resources for
create/update/etc operations has been corrected per the FHIR rules
outlined at
<a href="http://hl7.org/fhir/binary.html">Binary Resource</a> in both
the client and server.
<![CDATA[<br/><br/>]]>
Essentially, if the Binary contains something
that isn't FHIR (e.g. an image with an image content-type) the
client will send the raw data with the image content type to the server. The
server will place the content type and raw data into a Binary resource instance
and pass those to the resource provider. This part was already correct previous
to 1.5.
<![CDATA[<br/><br/>]]>
On the other hand, if the Binary contains a FHIR content type, the Binary
is now sent by the client to the server as a Binary resource with a FHIR content-type,
and the embedded FHIR content is contained in the appropriate fields. The server
will pass this "outer" Binary resource to the resource provider code.
</action>
</release>
<release version="1.4" date="2016-02-04">
<action type="add">

View File

@ -115,8 +115,9 @@
Update methods must be annotated with the
<a href="./apidocs/ca/uhn/fhir/rest/annotation/Update.html">@Update</a>
annotation, and have a parameter annotated with the
<a href="./apidocs/ca/uhn/fhir/rest/annotation/ResourceParam.html">@Resource</a>
<a href="./apidocs/ca/uhn/fhir/rest/annotation/ResourceParam.html">@ResourceParam</a>
annotation. This parameter contains the resource instance to be created.
See the <a href="./apidocs/ca/uhn/fhir/rest/annotation/ResourceParam.html">@ResourceParam</a> for information on the types allowed for this parameter (resource types, String, byte[]).
</p>
<p>
In addition, the method may optionally have a parameter annotated with the
@ -316,8 +317,9 @@
Create methods must be annotated with the
<a href="./apidocs/ca/uhn/fhir/rest/annotation/Create.html">@Create</a>
annotation, and have a single parameter annotated with the
<a href="./apidocs/ca/uhn/fhir/rest/annotation/ResourceParam.html">@Resource</a>
<a href="./apidocs/ca/uhn/fhir/rest/annotation/ResourceParam.html">@ResourceParam</a>
annotation. This parameter contains the resource instance to be created.
See the <a href="./apidocs/ca/uhn/fhir/rest/annotation/ResourceParam.html">@ResourceParam</a> for information on the types allowed for this parameter (resource types, String, byte[]).
</p>
<p>
Create methods must return an object of type
@ -1104,7 +1106,7 @@
Validate methods must be annotated with the
<a href="./apidocs/ca/uhn/fhir/rest/annotation/Validate.html">@Validate</a>
annotation, and have a parameter annotated with the
<a href="./apidocs/ca/uhn/fhir/rest/annotation/ResourceParam.html">@Resource</a>
<a href="./apidocs/ca/uhn/fhir/rest/annotation/ResourceParam.html">@ResourceParam</a>
annotation. This parameter contains the resource instance to be created.
</p>
<p>