Fix #297 - Resource not populated in ActionRequestDetails for validate method

This commit is contained in:
jamesagnew 2016-02-19 06:21:05 -05:00
parent 6cc60624c6
commit 43bdfc0345
10 changed files with 210 additions and 28 deletions

View File

@ -243,6 +243,7 @@ public class FhirContext {
* for extending the core library.
*/
public RuntimeResourceDefinition getResourceDefinition(IBaseResource theResource) {
Validate.notNull(theResource, "theResource must not be null");
return getResourceDefinition(theResource.getClass());
}

View File

@ -303,8 +303,7 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
* The method params as generated by the specific method binding
*/
protected void populateActionRequestDetailsForInterceptor(RequestDetails theRequestDetails, ActionRequestDetails theDetails, Object[] theMethodParams) {
// TODO Auto-generated method stub
// nothing by default
}
protected BaseServerResponseException processNon2xxResponseAndReturnExceptionToThrow(int theStatusCode, String theResponseMimeType, Reader theResponseReader) {

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.rest.method;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
@ -50,11 +51,13 @@ import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.client.BaseHttpClientInvocation;
import ca.uhn.fhir.rest.param.ResourceParameter;
import ca.uhn.fhir.rest.server.IBundleProvider;
import ca.uhn.fhir.rest.server.IRestfulServer;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.util.FhirTerser;
public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
@ -62,6 +65,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationMethodBinding.class);
private boolean myCanOperateAtInstanceLevel;
private boolean myCanOperateAtServerLevel;
private boolean myCanOperateAtTypeLevel;
private String myDescription;
private final boolean myIdempotent;
private final Integer myIdParamIndex;
@ -69,7 +73,6 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
private final RestOperationTypeEnum myOtherOperatiopnType;
private List<ReturnType> myReturnParams;
private final ReturnTypeEnum myReturnType;
private boolean myCanOperateAtTypeLevel;
protected OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, boolean theIdempotent, String theOperationName, Class<? extends IBaseResource> theOperationType,
OperationParam[] theReturnParams) {
@ -86,7 +89,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
} else {
myCanOperateAtTypeLevel = true;
}
Description description = theMethod.getAnnotation(Description.class);
if (description != null) {
myDescription = description.formalDefinition();
@ -179,16 +182,16 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
return myName;
}
@Override
public RestOperationTypeEnum getRestOperationType() {
return myOtherOperatiopnType;
}
@Override
protected BundleTypeEnum getResponseBundleType() {
return BundleTypeEnum.COLLECTION;
}
@Override
public RestOperationTypeEnum getRestOperationType() {
return myOtherOperatiopnType;
}
public List<ReturnType> getReturnParams() {
return Collections.unmodifiableList(myReturnParams);
}
@ -211,7 +214,13 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
if (!myName.equals(theRequest.getOperation())) {
return false;
}
RequestTypeEnum requestType = theRequest.getRequestType();
if (requestType != RequestTypeEnum.GET && requestType != RequestTypeEnum.POST) {
// Operations can only be invoked with GET and POST
return false;
}
boolean requestHasId = theRequest.getId() != null;
if (requestHasId) {
if (isCanOperateAtInstanceLevel() == false) {
@ -246,9 +255,9 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
}
@Override
public Object invokeServer(IRestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) throws BaseServerResponseException {
public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws BaseServerResponseException {
if (theRequest.getRequestType() == RequestTypeEnum.POST) {
// always ok
// all good
} else if (theRequest.getRequestType() == RequestTypeEnum.GET) {
if (!myIdempotent) {
String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class, "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.POST.name());
@ -273,6 +282,15 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
return retVal;
}
@Override
public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException {
if (theRequest.getRequestType() == RequestTypeEnum.POST) {
IBaseResource requestContents = ResourceParameter.loadResourceFromRequest(theRequest, this, null);
theRequest.getUserData().put(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY, requestContents);
}
return super.invokeServer(theServer, theRequest);
}
public boolean isCanOperateAtInstanceLevel() {
return this.myCanOperateAtInstanceLevel;
}
@ -285,6 +303,12 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
return myIdempotent;
}
@Override
protected void populateActionRequestDetailsForInterceptor(RequestDetails theRequestDetails, ActionRequestDetails theDetails, Object[] theMethodParams) {
super.populateActionRequestDetailsForInterceptor(theRequestDetails, theDetails, theMethodParams);
theDetails.setResource((IBaseResource) theRequestDetails.getUserData().get(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY));
}
public void setDescription(String theDescription) {
myDescription = theDescription;
}

View File

@ -50,7 +50,6 @@ import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.ValidationModeEnum;
import ca.uhn.fhir.rest.param.CollectionBinder;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.ResourceParameter;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
@ -59,6 +58,8 @@ import ca.uhn.fhir.util.ParametersUtil;
public class OperationParameter implements IParameter {
static final String REQUEST_CONTENTS_USERDATA_KEY = OperationParam.class.getName() + "_PARSED_RESOURCE";
private IConverter myConverter;
@SuppressWarnings("rawtypes")
private Class<? extends Collection> myInnerCollectionType;
@ -210,14 +211,7 @@ public class OperationParameter implements IParameter {
FhirContext ctx = theRequest.getServer().getFhirContext();
if (theRequest.getRequestType() == RequestTypeEnum.GET) {
return null;
}
// Class<? extends IBaseResource> wantedResourceType = theMethodBinding.getContext().getResourceDefinition("Parameters").getImplementingClass();
Class<IBaseResource> wantedResourceType = null;
IBaseResource requestContents = ResourceParameter.loadResourceFromRequest(theRequest, theMethodBinding, wantedResourceType);
IBaseResource requestContents = (IBaseResource) theRequest.getUserData().get(REQUEST_CONTENTS_USERDATA_KEY);
RuntimeResourceDefinition def = ctx.getResourceDefinition(requestContents);
if (def.getName().equals("Parameters")) {

View File

@ -34,6 +34,8 @@ import ca.uhn.fhir.rest.client.BaseHttpClientInvocation;
import ca.uhn.fhir.rest.param.ResourceParameter;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.EncodingEnum;
import ca.uhn.fhir.rest.server.IRestfulServer;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.util.ParametersUtil;
public class ValidateMethodBindingDstu2 extends OperationMethodBinding {

View File

@ -621,10 +621,12 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
}
/*
* Actualy invoke the server method. This call is to a HAPI method binding, which is an object that wraps a specific implementing (user-supplied) method, but handles its input and provides
* its output back to the client.
* Actualy invoke the server method. This call is to a HAPI method binding, which
* is an object that wraps a specific implementing (user-supplied) method, but
* handles its input and provides its output back to the client.
*
* This is basically the end of processing for a successful request, since the method binding replies to the client and closes the response.
* This is basically the end of processing for a successful request, since the
* method binding replies to the client and closes the response.
*/
resourceMethod.invokeServer(this, requestDetails);

View File

@ -45,9 +45,6 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.util.PortUtil;
/**
* Created by dsotnikov on 2/25/2014.
*/
public class InterceptorTest {
private static CloseableHttpClient ourClient;

View File

@ -0,0 +1,157 @@
package ca.uhn.fhir.rest.server;
import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
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.Patient;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Validate;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.method.RequestDetails;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.util.PortUtil;
public class InterceptorDstu3Test {
private static CloseableHttpClient ourClient;
private static int ourPort;
private static Server ourServer;
private static RestfulServer servlet;
private IServerInterceptor myInterceptor1;
private IServerInterceptor myInterceptor2;
private static final FhirContext ourCtx = FhirContext.forDstu3();
@Test
public void testValidate() throws Exception {
when(myInterceptor1.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myInterceptor1.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myInterceptor1.outgoingResponse(any(RequestDetails.class), any(IResource.class))).thenReturn(true);
when(myInterceptor2.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myInterceptor2.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myInterceptor2.outgoingResponse(any(RequestDetails.class), any(IResource.class))).thenReturn(true);
//@formatter:off
String input =
"{\n" +
" \"resourceType\":\"Observation\",\n" +
" \"id\":\"1855669\",\n" +
" \"meta\":{\n" +
" \"versionId\":\"1\",\n" +
" \"lastUpdated\":\"2016-02-18T07:41:35.953-05:00\"\n" +
" },\n" +
" \"status\":\"final\",\n" +
" \"subject\":{\n" +
" \"reference\":\"Patient/1\"\n" +
" },\n" +
" \"effectiveDateTime\":\"2016-02-18T07:45:36-05:00\",\n" +
" \"valueQuantity\":{\n" +
" \"value\":57,\n" +
" \"system\":\"http://unitsofmeasure.org\",\n" +
" \"code\":\"{Beats}/min\"\n" +
" }\n" +
"}";
//@formatter:on
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$validate");
httpPost.setEntity(new StringEntity(input, ContentType.create(Constants.CT_FHIR_JSON, "UTF-8")));
HttpResponse status = ourClient.execute(httpPost);
IOUtils.closeQuietly(status.getEntity().getContent());
InOrder order = inOrder(myInterceptor1, myInterceptor2);
order.verify(myInterceptor1, times(1)).incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class));
order.verify(myInterceptor2, times(1)).incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class));
order.verify(myInterceptor1, times(1)).incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class));
order.verify(myInterceptor2, times(1)).incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class));
ArgumentCaptor<RestOperationTypeEnum> opTypeCapt = ArgumentCaptor.forClass(RestOperationTypeEnum.class);
ArgumentCaptor<ActionRequestDetails> arTypeCapt = ArgumentCaptor.forClass(ActionRequestDetails.class);
order.verify(myInterceptor1, times(1)).incomingRequestPreHandled(opTypeCapt.capture(), arTypeCapt.capture());
order.verify(myInterceptor2, times(1)).incomingRequestPreHandled(any(RestOperationTypeEnum.class), any(ActionRequestDetails.class));
order.verify(myInterceptor2, times(1)).outgoingResponse(any(RequestDetails.class), any(IResource.class));
order.verify(myInterceptor1, times(1)).outgoingResponse(any(RequestDetails.class), any(IResource.class));
verifyNoMoreInteractions(myInterceptor1);
verifyNoMoreInteractions(myInterceptor2);
assertEquals(RestOperationTypeEnum.EXTENDED_OPERATION_TYPE, opTypeCapt.getValue());
assertNotNull(arTypeCapt.getValue().getResource());
}
@AfterClass
public static void afterClass() throws Exception {
ourServer.stop();
}
@Before
public void before() {
myInterceptor1 = mock(IServerInterceptor.class);
myInterceptor2 = mock(IServerInterceptor.class);
servlet.setInterceptors(myInterceptor1, myInterceptor2);
}
@BeforeClass
public static void beforeClass() throws Exception {
ourPort = PortUtil.findFreePort();
ourServer = new Server(ourPort);
DummyPatientResourceProvider patientProvider = new DummyPatientResourceProvider();
ServletHandler proxyHandler = new ServletHandler();
servlet = new RestfulServer(ourCtx);
servlet.setResourceProviders(patientProvider);
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 DummyPatientResourceProvider implements IResourceProvider {
@Validate()
public MethodOutcome validate(@ResourceParam Patient theResource) {
return new MethodOutcome();
}
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
}
}

View File

@ -71,6 +71,12 @@
interceptor wants to pass the logged in user's details to other parts
of the server.
</action>
<action type="fix" issue="297">
<![CDATA[When <code>IServerInterceptor#incomingRequestPreHandled()</code> is called
for a <code>@Validate</code> method, the resource was not populated in the
<code>ActionRequestDetails</code> argument. Thanks to Ravi Kuchi for reporting!
]]>
</action>
</release>
<release version="1.4" date="2016-02-04">
<action type="add">

View File

@ -92,7 +92,7 @@
</p>
<macro name="snippet">
<param name="id" value="cookie" />
<param name="id" value="gzip" />
<param name="file" value="examples/src/main/java/example/ClientExamples.java" />
</macro>