Correct handling of Binary resources in client and server where the
binary contains embedded FHIR content
This commit is contained in:
parent
4a26064cd6
commit
d383b402d1
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue