Make server operation interceptor not perform superfluous calls for

conditional and no-op operations
This commit is contained in:
jamesagnew 2018-03-22 06:48:08 -04:00
parent b89e0e7020
commit 1689dc889e
6 changed files with 210 additions and 54 deletions

View File

@ -130,19 +130,6 @@ public class GenericClient extends BaseClient implements IGenericClient {
} }
// public IResource read(UriDt url) {
// return read(inferResourceClass(url), url);
// }
//
// @SuppressWarnings("unchecked")
// public <T extends IResource> T read(final Class<T> theType, UriDt url) {
// return (T) invoke(theType, url, new ResourceResponseHandler<T>(theType));
// }
//
// public Bundle search(UriDt url) {
// return search(inferResourceClass(url), url);
// }
@Override @Override
public IFetchConformanceUntyped fetchConformance() { public IFetchConformanceUntyped fetchConformance() {
return new FetchConformanceInternal(); return new FetchConformanceInternal();
@ -169,11 +156,6 @@ public class GenericClient extends BaseClient implements IGenericClient {
return theResource.getIdElement().getIdPart(); return theResource.getIdElement().getIdPart();
} }
// @Override
// public <T extends IBaseResource> T read(final Class<T> theType, IdDt theId) {
// return doReadOrVRead(theType, theId, false, null, null);
// }
@Override @Override
public IHistory history() { public IHistory history() {
return new HistoryInternal(); return new HistoryInternal();
@ -2105,14 +2087,14 @@ public class GenericClient extends BaseClient implements IGenericClient {
@Override @Override
public IUpdateTyped resource(IBaseResource theResource) { public IUpdateTyped resource(IBaseResource theResource) {
//Validate.notNull(theResource, "Resource can not be null"); Validate.notNull(theResource, "Resource can not be null");
myResource = theResource; myResource = theResource;
return this; return this;
} }
@Override @Override
public IUpdateTyped resource(String theResourceBody) { public IUpdateTyped resource(String theResourceBody) {
//Validate.notBlank(theResourceBody, "Body can not be null or blank"); Validate.notBlank(theResourceBody, "Body can not be null or blank");
myResourceBody = theResourceBody; myResourceBody = theResourceBody;
return this; return this;
} }

View File

@ -431,7 +431,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
} }
// Perform actual DB update // Perform actual DB update
updateEntity(theResource, entity, null, thePerformIndexing, thePerformIndexing, theUpdateTime, false, thePerformIndexing); ResourceTable updatedEntity = updateEntity(theResource, entity, null, thePerformIndexing, thePerformIndexing, theUpdateTime, false, thePerformIndexing);
theResource.setId(entity.getIdDt()); theResource.setId(entity.getIdDt());
@ -445,12 +445,14 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
} }
// Notify JPA interceptors // Notify JPA interceptors
if (theRequestDetails != null) { if (!updatedEntity.isUnchangedInCurrentOperation()) {
theRequestDetails.getRequestOperationCallback().resourceCreated(theResource); if (theRequestDetails != null) {
} theRequestDetails.getRequestOperationCallback().resourceCreated(theResource);
for (IServerInterceptor next : getConfig().getInterceptors()) { }
if (next instanceof IServerOperationInterceptor) { for (IServerInterceptor next : getConfig().getInterceptors()) {
((IServerOperationInterceptor) next).resourceCreated(theRequestDetails, theResource); if (next instanceof IServerOperationInterceptor) {
((IServerOperationInterceptor) next).resourceCreated(theRequestDetails, theResource);
}
} }
} }
@ -1262,14 +1264,16 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
} }
// Notify interceptors // Notify interceptors
if (theRequestDetails != null) { if (!savedEntity.isUnchangedInCurrentOperation()) {
theRequestDetails.getRequestOperationCallback().resourceUpdated(theResource); if (theRequestDetails != null) {
theRequestDetails.getRequestOperationCallback().resourceUpdated(oldResource, theResource); theRequestDetails.getRequestOperationCallback().resourceUpdated(theResource);
} theRequestDetails.getRequestOperationCallback().resourceUpdated(oldResource, theResource);
for (IServerInterceptor next : getConfig().getInterceptors()) { }
if (next instanceof IServerOperationInterceptor) { for (IServerInterceptor next : getConfig().getInterceptors()) {
((IServerOperationInterceptor) next).resourceUpdated(theRequestDetails, theResource); if (next instanceof IServerOperationInterceptor) {
((IServerOperationInterceptor) next).resourceUpdated(theRequestDetails, oldResource, theResource); ((IServerOperationInterceptor) next).resourceUpdated(theRequestDetails, theResource);
((IServerOperationInterceptor) next).resourceUpdated(theRequestDetails, oldResource, theResource);
}
} }
} }

View File

@ -8,6 +8,7 @@ import ca.uhn.fhir.rest.api.server.ResponseDetails;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor;
import ca.uhn.fhir.rest.server.interceptor.InterceptorAdapter; import ca.uhn.fhir.rest.server.interceptor.InterceptorAdapter;
import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.TestUtil;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
@ -43,9 +44,9 @@ import static org.mockito.Mockito.*;
public class ResourceProviderInterceptorR4Test extends BaseResourceProviderR4Test { public class ResourceProviderInterceptorR4Test extends BaseResourceProviderR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceProviderInterceptorR4Test.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceProviderInterceptorR4Test.class);
private IServerInterceptor myDaoInterceptor; private IServerOperationInterceptor myDaoInterceptor;
private IServerInterceptor myServerInterceptor; private IServerOperationInterceptor myServerInterceptor;
@Override @Override
@After @After
@ -60,8 +61,8 @@ public class ResourceProviderInterceptorR4Test extends BaseResourceProviderR4Tes
public void before() throws Exception { public void before() throws Exception {
super.before(); super.before();
myServerInterceptor = mock(IServerInterceptor.class); myServerInterceptor = mock(IServerOperationInterceptor.class);
myDaoInterceptor = mock(IServerInterceptor.class); myDaoInterceptor = mock(IServerOperationInterceptor.class);
resetServerInterceptor(); resetServerInterceptor();
@ -88,6 +89,57 @@ public class ResourceProviderInterceptorR4Test extends BaseResourceProviderR4Tes
when(myServerInterceptor.outgoingResponse(any(RequestDetails.class), any(ResponseDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(myServerInterceptor.outgoingResponse(any(RequestDetails.class), any(ResponseDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
} }
@Test
public void testCreateConditionalNoOpResourceInTransaction() throws Exception {
String methodName = "foo";
Patient pt = new Patient();
pt.addName().setFamily(methodName);
Bundle bundle = new Bundle();
bundle.setType(BundleType.TRANSACTION);
BundleEntryComponent entry = bundle.addEntry();
entry.setFullUrl("Patient");
entry.setResource(pt);
entry.getRequest().setMethod(HTTPVerb.POST);
entry.getRequest().setUrl("Patient");
// Transaction time!
transaction(bundle);
// Do it again but with a conditional create that shouldn't actually create
resetServerInterceptor();
entry.getRequest().setIfNoneExist("Patient?name=" + methodName);
transaction(bundle);
/*
* Server Interceptor
*/
ArgumentCaptor<ActionRequestDetails> ardCaptor = ArgumentCaptor.forClass(ActionRequestDetails.class);
ArgumentCaptor<RestOperationTypeEnum> opTypeCaptor = ArgumentCaptor.forClass(RestOperationTypeEnum.class);
verify(myServerInterceptor, times(1)).incomingRequestPreHandled(opTypeCaptor.capture(), ardCaptor.capture());
assertEquals(RestOperationTypeEnum.TRANSACTION, opTypeCaptor.getAllValues().get(0));
verify(myServerInterceptor, times(1)).incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class));
verify(myServerInterceptor, times(0)).resourceCreated(any(RequestDetails.class), any(IBaseResource.class));
verify(myServerInterceptor, times(0)).resourceUpdated(any(RequestDetails.class), any(IBaseResource.class), any(IBaseResource.class));
/*
* DAO Interceptor
*/
ardCaptor = ArgumentCaptor.forClass(ActionRequestDetails.class);
opTypeCaptor = ArgumentCaptor.forClass(RestOperationTypeEnum.class);
verify(myDaoInterceptor, times(1)).incomingRequestPreHandled(opTypeCaptor.capture(), ardCaptor.capture());
assertEquals(RestOperationTypeEnum.TRANSACTION, opTypeCaptor.getAllValues().get(0));
verify(myDaoInterceptor, times(0)).incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class));
verify(myDaoInterceptor, times(0)).resourceCreated(any(RequestDetails.class), any(IBaseResource.class));
verify(myDaoInterceptor, times(0)).resourceUpdated(any(RequestDetails.class), any(IBaseResource.class), any(IBaseResource.class));
}
@Test @Test
public void testCreateResource() throws IOException { public void testCreateResource() throws IOException {
String methodName = "testCreateResource"; String methodName = "testCreateResource";
@ -123,11 +175,11 @@ public class ResourceProviderInterceptorR4Test extends BaseResourceProviderR4Tes
} }
/* /*
* This is a weird way of verifying, but because this class * This is a weird way of verifying, but because this class
* is a child of a superclass that has other test children * is a child of a superclass that has other test children
* it's possible that other tests are hitting the server * it's possible that other tests are hitting the server
* at the same time * at the same time
*/ */
@Test @Test
public void testCreateResourceInTransaction() throws IOException { public void testCreateResourceInTransaction() throws IOException {
String methodName = "testCreateResourceInTransaction"; String methodName = "testCreateResourceInTransaction";
@ -143,16 +195,7 @@ public class ResourceProviderInterceptorR4Test extends BaseResourceProviderR4Tes
entry.getRequest().setMethod(HTTPVerb.POST); entry.getRequest().setMethod(HTTPVerb.POST);
entry.getRequest().setUrl("Patient"); entry.getRequest().setUrl("Patient");
String resource = myFhirCtx.newXmlParser().encodeResourceToString(bundle); transaction(bundle);
HttpPost post = new HttpPost(ourServerBase + "/");
post.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
CloseableHttpResponse response = ourHttpClient.execute(post);
try {
assertEquals(200, response.getStatusLine().getStatusCode());
} finally {
response.close();
}
/* /*
* Server Interceptor * Server Interceptor
@ -246,6 +289,72 @@ public class ResourceProviderInterceptorR4Test extends BaseResourceProviderR4Tes
} }
@Test
public void testUpdateNoOpResourceInTransaction() throws Exception {
String methodName = "foo";
Patient pt = new Patient();
pt.addName().setFamily(methodName);
String ptId = myPatientDao.create(pt).getId().toUnqualifiedVersionless().getValue();
Bundle bundle = new Bundle();
bundle.setType(BundleType.TRANSACTION);
BundleEntryComponent entry = bundle.addEntry();
entry.setFullUrl(ptId);
entry.setResource(pt);
entry.getRequest().setMethod(HTTPVerb.PUT);
entry.getRequest().setUrl(ptId);
// Transaction time!
transaction(bundle);
// Do it again but with an update that shouldn't actually create
resetServerInterceptor();
entry.getRequest().setIfNoneExist("Patient?name=" + methodName);
transaction(bundle);
/*
* Server Interceptor
*/
ArgumentCaptor<ActionRequestDetails> ardCaptor = ArgumentCaptor.forClass(ActionRequestDetails.class);
ArgumentCaptor<RestOperationTypeEnum> opTypeCaptor = ArgumentCaptor.forClass(RestOperationTypeEnum.class);
verify(myServerInterceptor, times(2)).incomingRequestPreHandled(opTypeCaptor.capture(), ardCaptor.capture());
assertEquals(RestOperationTypeEnum.TRANSACTION, opTypeCaptor.getAllValues().get(0));
assertEquals(RestOperationTypeEnum.UPDATE, opTypeCaptor.getAllValues().get(1));
verify(myServerInterceptor, times(1)).incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class));
verify(myServerInterceptor, times(0)).resourceCreated(any(RequestDetails.class), any(IBaseResource.class));
verify(myServerInterceptor, times(0)).resourceUpdated(any(RequestDetails.class), any(IBaseResource.class), any(IBaseResource.class));
/*
* DAO Interceptor
*/
ardCaptor = ArgumentCaptor.forClass(ActionRequestDetails.class);
opTypeCaptor = ArgumentCaptor.forClass(RestOperationTypeEnum.class);
verify(myDaoInterceptor, times(2)).incomingRequestPreHandled(opTypeCaptor.capture(), ardCaptor.capture());
assertEquals(RestOperationTypeEnum.TRANSACTION, opTypeCaptor.getAllValues().get(0));
assertEquals(RestOperationTypeEnum.UPDATE, opTypeCaptor.getAllValues().get(1));
verify(myDaoInterceptor, times(0)).incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class));
verify(myDaoInterceptor, times(0)).resourceCreated(any(RequestDetails.class), any(IBaseResource.class));
verify(myDaoInterceptor, times(0)).resourceUpdated(any(RequestDetails.class), any(IBaseResource.class), any(IBaseResource.class));
}
private void transaction(Bundle theBundle) throws IOException {
String resource = myFhirCtx.newXmlParser().encodeResourceToString(theBundle);
HttpPost post = new HttpPost(ourServerBase + "/");
post.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
CloseableHttpResponse response = ourHttpClient.execute(post);
try {
assertEquals(200, response.getStatusLine().getStatusCode());
} finally {
response.close();
}
}
@AfterClass @AfterClass
public static void afterClassClearContext() { public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest(); TestUtil.clearAllStaticFieldsForUnitTest();

View File

@ -135,6 +135,41 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test {
waitForSize(0, ourCreatedObservations); waitForSize(0, ourCreatedObservations);
waitForSize(1, ourUpdatedObservations); waitForSize(1, ourUpdatedObservations);
assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0)); assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0));
}
@Test
public void testRestHookSubscriptionNoopUpdateDoesntTriggerNewDelivery() throws Exception {
String payload = "application/fhir+json";
String code = "1000000050";
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
createSubscription(criteria1, payload, ourListenerServerBase);
createSubscription(criteria2, payload, ourListenerServerBase);
Observation obs = sendObservation(code, "SNOMED-CT");
// Should see 1 subscription notification
waitForQueueToDrain();
waitForSize(0, ourCreatedObservations);
waitForSize(1, ourUpdatedObservations);
assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0));
// Send an update with no changes
obs.setId(obs.getIdElement().toUnqualifiedVersionless());
myClient.update().resource(obs).execute();
// Should be no further deliveries
Thread.sleep(1000);
waitForQueueToDrain();
waitForSize(0, ourCreatedObservations);
waitForSize(1, ourUpdatedObservations);
} }
@Test @Test

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.jpa.search; package ca.uhn.fhir.jpa.search;
/*-
* #%L
* HAPI FHIR JPA Server - ElasticSearch Integration
* %%
* Copyright (C) 2014 - 2018 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.hibernate.search.elasticsearch.analyzer.definition.ElasticsearchAnalysisDefinitionRegistryBuilder; import org.hibernate.search.elasticsearch.analyzer.definition.ElasticsearchAnalysisDefinitionRegistryBuilder;
import org.hibernate.search.elasticsearch.analyzer.definition.spi.ElasticsearchAnalysisDefinitionProvider; import org.hibernate.search.elasticsearch.analyzer.definition.spi.ElasticsearchAnalysisDefinitionProvider;

View File

@ -240,6 +240,12 @@
is supported according to the FHIR specification. Thanks is supported according to the FHIR specification. Thanks
to Jeff Chung for the pull request! to Jeff Chung for the pull request!
</action> </action>
<action type="fix">
JPA Server Operation Interceptor create/update methods will now no
longer be fired if the create/update operation being performed
is a no-op (e.g. a conditional create that did not need to perform
any action, or an update where the contents didn't actually change)
</action>
</release> </release>
<release version="3.2.0" date="2018-01-13"> <release version="3.2.0" date="2018-01-13">
<action type="add"> <action type="add">